【问题标题】:Serialize double and float with C用 C 序列化 double 和 float
【发布时间】:2010-08-05 19:15:26
【问题描述】:

如何在 C 中序列化双精度和浮点数?

我有以下用于序列化 short、int 和 char 的代码。

unsigned char * serialize_char(unsigned char *buffer, char value)
{
    buffer[0] = value;
    return buffer + 1;
}

unsigned char * serialize_int(unsigned char *buffer, int value)
{
    buffer[0] = value >> 24;
    buffer[1] = value >> 16;
    buffer[2] = value >> 8;
    buffer[3] = value;
    return buffer + 4;
}

unsigned char * serialize_short(unsigned char *buffer, short value)
{
    buffer[0] = value >> 8;
    buffer[1] = value;
    return buffer + 2;
}

编辑:

我从this question找到了这些函数

编辑 2:

序列化的目的是将数据发送到UDP套接字,并保证即使字节序不同,也可以在另一台机器上进行反序列化。鉴于我必须序列化 int、double、float 和 char*,是否还有其他“最佳实践”来执行此功能?

【问题讨论】:

  • 这似乎毫无意义 - 您最终会得到一个包含与数字大小相同的数字的缓冲区。您认为这些功能实现了什么?例如,为什么不使用 memcpy()?
  • @Neil Butterworth 他的功能独立于主机端。
  • @Neil Butterworth 是的,CODE 确定最高有效字节写入最低地址,而不是主机字节序
  • 好吧,他们试图这样做,但假设 int 是 int32_t 等,并且应该更加注意有符号 int 的右移。
  • 感谢他意识到他完全需要这样做......我已经看到了许多定义的“文件格式”,而不考虑字节顺序或 sizeof(int) 计数。由于他仅将右移结果的最小字节分配给 unsigned char,因此应该一切正常。不过,如果 sizeof(int)==2 在某个平台上,他就完蛋了。

标签: c serialization floating-point


【解决方案1】:

便携方式:使用frexp进行序列化(转换为整数尾数和指数),ldexp进行反序列化。

简单的方法:假设 2010 年您关心的任何机器都使用 IEEE 浮点数,声明一个带有 float 元素和 uint32_t 元素的联合,并使用您的整数序列化代码序列化浮点数。

二进制文件讨厌的方式:将所有内容序列化为文本,包括浮点数。使用"%a" printf 格式说明符来获得一个十六进制浮点数,它总是精确地表达(前提是您没有使用"%.4a" 之类的东西限制精度)并且不会出现舍入错误。您可以使用strtod 或任何scanf 系列函数来阅读这些内容。

【讨论】:

  • %a 不在 C89 中,但在 C99 中。值得注意的是,C99 还通过指定 printf 如何格式化它们和 scanf 读取它们来更好地处理 NaN 和无穷大。
  • 好点。如果您需要 C89 兼容性,只需编写您自己的 printf("%a", f) 代码即可。如果您不需要对非有限参数的支持,它只需要大约 20 行,如果需要,则需要 10-15 行。与以十进制打印浮点数不同,以十六进制打印它们非常容易,并且简单的实现可以满足您的期望(即它确实有效)。
  • frexp 返回整数指数,但是如何将尾数作为整数?
  • frexp 返回 [1,2) 范围内的尾数。所以只需按 2^23 或 2^52 缩放,然后转换为适当的整数类型。
  • @R.. 实际上,我相信它在 [0.5,1) 范围内,不是吗?
【解决方案2】:

我记得第一次看到下面示例中使用的演员表是在“rsqrt”例程的良好旧 Quake 源代码中,其中包含我当时看到的最酷的评论(谷歌它,你会喜欢它)

unsigned char * serialize_float(unsigned char *buffer, float value) 
{ 
    unsigned int ivalue = *((unsigned int*)&value); // warning assumes 32-bit "unsigned int"
    buffer[0] = ivalue >> 24;  
    buffer[1] = ivalue >> 16;  
    buffer[2] = ivalue >> 8;  
    buffer[3] = ivalue;  
    return buffer + 4; 
} 

我希望我正确理解了您的问题(和示例代码)。让我知道这是否有用?

【讨论】:

  • 我会在函数顶部添加char assumes_sz_float_eq_sz_int[(2*(int)(sizeof(int)==sizeof(float)))-1];
  • @David X 作为编译时检查?好主意,我通常用枚举来做这个技巧,但我猜负数组长度同样适用
  • 对于那些寻找评论的人,请参阅this Wikipedia 链接
  • 这是一个“类型双关语”的例子,它不是超级安全的。 SO上有很多例子,例如stackoverflow.com/questions/222266/…
  • “但从未遇到过编译器” 基于别名的优化有一个坏习惯,即大多数时间生成看似有效的代码。问题是没有保证。 “批评之后还没有(在我看来)更好的解决方案。” 标准中明确定义了通过 union 或使用memcpy 复制的类型双关语。
【解决方案3】:

这会将浮点值打包到 intlong long 对中,然后您可以将其与其他函数序列化。 unpack() 函数用于反序列化。

这对数字分别代表数字的指数部分和小数部分。

#define FRAC_MAX 9223372036854775807LL /* 2**63 - 1 */

struct dbl_packed
{
    int exp;
    long long frac;
};

void pack(double x, struct dbl_packed *r)
{
    double xf = fabs(frexp(x, &r->exp)) - 0.5;

    if (xf < 0.0)
    {
        r->frac = 0;
        return;
    }

    r->frac = 1 + (long long)(xf * 2.0 * (FRAC_MAX - 1));

    if (x < 0.0)
        r->frac = -r->frac;
}

double unpack(const struct dbl_packed *p)
{
    double xf, x;

    if (p->frac == 0)
        return 0.0;

    xf = ((double)(llabs(p->frac) - 1) / (FRAC_MAX - 1)) / 2.0;

    x = ldexp(xf + 0.5, p->exp);

    if (p->frac < 0)
        x = -x;

    return x;
}

【讨论】:

    【解决方案4】:

    无论原生表示如何,您都可以在 IEEE-754 中进行可移植序列化:

    int fwriteieee754(double x, FILE * fp, int bigendian)
    {
        int                     shift;
        unsigned long           sign, exp, hibits, hilong, lowlong;
        double                  fnorm, significand;
        int                     expbits = 11;
        int                     significandbits = 52;
    
        /* zero (can't handle signed zero) */
        if(x == 0) {
            hilong = 0;
            lowlong = 0;
            goto writedata;
        }
        /* infinity */
        if(x > DBL_MAX) {
            hilong = 1024 + ((1 << (expbits - 1)) - 1);
            hilong <<= (31 - expbits);
            lowlong = 0;
            goto writedata;
        }
        /* -infinity */
        if(x < -DBL_MAX) {
            hilong = 1024 + ((1 << (expbits - 1)) - 1);
            hilong <<= (31 - expbits);
            hilong |= (1 << 31);
            lowlong = 0;
            goto writedata;
        }
        /* NaN - dodgy because many compilers optimise out this test
         * isnan() is C99, POSIX.1 only, use it if you will.
         */
        if(x != x) {
            hilong = 1024 + ((1 << (expbits - 1)) - 1);
            hilong <<= (31 - expbits);
            lowlong = 1234;
            goto writedata;
        }
    
        /* get the sign */
        if(x < 0) {
            sign = 1;
            fnorm = -x;
        } else {
            sign = 0;
            fnorm = x;
        }
    
        /* get the normalized form of f and track the exponent */
        shift = 0;
        while(fnorm >= 2.0) {
            fnorm /= 2.0;
            shift++;
        }
        while(fnorm < 1.0) {
            fnorm *= 2.0;
            shift--;
        }
    
        /* check for denormalized numbers */
        if(shift < -1022) {
            while(shift < -1022) {
                fnorm /= 2.0;
                shift++;
            }
            shift = -1023;
        } else {
            /* take the significant bit off mantissa */
            fnorm = fnorm - 1.0;
        }
        /* calculate the integer form of the significand */
        /* hold it in a  double for now */
    
        significand = fnorm * ((1LL << significandbits) + 0.5f);
    
        /* get the biased exponent */
        exp = shift + ((1 << (expbits - 1)) - 1);   /* shift + bias */
    
        /* put the data into two longs */
        hibits = (long)(significand / 4294967296);  /* 0x100000000 */
        hilong = (sign << 31) | (exp << (31 - expbits)) | hibits;
        lowlong = (unsigned long)(significand - hibits * 4294967296);
    
     writedata:
        /* write the bytes out to the stream */
        if(bigendian) {
            fputc((hilong >> 24) & 0xFF, fp);
            fputc((hilong >> 16) & 0xFF, fp);
            fputc((hilong >> 8) & 0xFF, fp);
            fputc(hilong & 0xFF, fp);
    
            fputc((lowlong >> 24) & 0xFF, fp);
            fputc((lowlong >> 16) & 0xFF, fp);
            fputc((lowlong >> 8) & 0xFF, fp);
            fputc(lowlong & 0xFF, fp);
        } else {
            fputc(lowlong & 0xFF, fp);
            fputc((lowlong >> 8) & 0xFF, fp);
            fputc((lowlong >> 16) & 0xFF, fp);
            fputc((lowlong >> 24) & 0xFF, fp);
    
            fputc(hilong & 0xFF, fp);
            fputc((hilong >> 8) & 0xFF, fp);
            fputc((hilong >> 16) & 0xFF, fp);
            fputc((hilong >> 24) & 0xFF, fp);
        }
        return ferror(fp);
    }
    

    在使用 IEEE-754 的机器中(即常见情况),您只需一个 fread() 即可获取号码。否则,请自行解码字节(sign * 2^(exponent-127) * 1.mantissa)

    注意:在本机双精度比 IEEE 双精度更高的系统中进行序列化时,您可能会遇到低位错误。

    希望这会有所帮助。

    【讨论】:

    • 我们还在使用 goto 语句吗?
    【解决方案5】:

    对于关于float 的狭隘问题,请注意,您最终可能会假设线路的两端都使用相同的浮点表示。鉴于 IEEE-754 的普遍使用,这在今天可能是安全的,但请注意,一些当前的 DSP(我相信 blackfins)使用不同的表示。在过去,浮点的表示至少与硬件和库的制造商一样多,所以这是一个更大的问题。

    即使具有相同的表示形式,它也可能不会以相同的字节顺序存储。这将需要决定线路上的字节顺序,并在每一端调整代码。类型双关指针转换或联合将在实践中起作用。两者都在调用实现定义的行为,但只要您检查和测试这没什么大不了的。

    也就是说,文本通常是您在平台之间传输浮点数的朋友。诀窍是不要使用太多真正需要将其转换回来的字符。

    总而言之,我建议认真考虑使用像XDR 这样的库,它很健壮,已经存在了一段时间,并且已经在所有尖角和边缘情况下都经过摩擦。

    如果你坚持自己滚动,除了floatdouble 的表示之外,还要注意int 是16 位、32 位还是64 位等细微问题。

    【讨论】:

    • AFAIK 黑鳍没有 FPU。
    • @S.C,也许我记得当时有一个 TI DSP。毕竟,所有 IEEE-754 的成本仍然比您希望在 DSP 中实现的成本更高。
    【解决方案6】:

    更新后,您提到数据将使用 UDP 传输并要求最佳做法。我强烈建议将数据作为文本发送,甚至可能添加一些标记 (XML)。在传输线上调试与字节序相关的错误是在浪费每个人的时间

    就您问题的“最佳实践”部分,我只需 2 美分

    【讨论】:

    • 虽然发送纯文本会很好,但要求之一是使用尽可能少的带宽。
    • @Trevor 发送文本并不一定意味着额外的带宽。例如,以 int 形式发送整数 1(在您的平台上)需要 4 个字节,以文本形式发送时需要 2(假设是分隔符)。所以这趋于平衡。而且文本的处理和调试要简单得多。
    • Okidoki,然后使用我在之前的答案中显示的示例代码,让我知道它是否适合您
    • 最后,使用带有分隔符的纯文本是最简单的方法。与将浮点数和双精度序列化到消息中相比,每条消息只有几个额外的字节。谢谢。
    【解决方案7】:

    你总是可以使用联合来序列化:

    void serialize_double (unsigned char* buffer, double x) {
        int i;
        union {
            double         d;
            unsigned char  bytes[sizeof(double)];
        } u;
    
        u.d = x;
        for (i=0; i<sizeof(double); ++i)
            buffer[i] = u.bytes[i];
    }
    

    这实际上并不比简单地将double 的地址转换为char* 更健壮,但至少通过在整个代码中使用sizeof() 可以避免数据类型占用更多/比您想象的要少(如果您在使用不同大小的double 的平台之间移动数据,这将无济于事)。

    对于浮点数,只需将所有 double 实例替换为 float。您可以构建一个巧妙的宏来自动生成一系列这些函数,每个函数对应您感兴趣的每种数据类型。

    【讨论】:

      【解决方案8】:

      首先,您不应该假设shortint 等在两侧具有相同的宽度。最好使用两边都知道宽度的uint32_t 等(无符号)类型。

      然后,为了确保您没有字节序问题,有一些宏/函数ntohhtos 等通常比您自己可以做的任何事情都更有效率。 (在英特尔硬件上,它们只是一条汇编指令。)因此您不必编写转换函数,基本上它们已经存在,只需将您的 buffer 指针转换为正确整数类型的指针。

      对于float,您可能会认为它们是 32 位的,并且在两边都有相同的表示。所以我认为一个好的策略是使用指向uint32_t* 的指针,然后使用与上述相同的策略。

      如果您认为float 可能有不同的表示形式,则必须拆分为尾数和指数。也许你可以使用frexpf

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-06-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-11-30
        相关资源
        最近更新 更多