【问题标题】:Portable serialisation of IEEE754 floating-point valuesIEEE754 浮点值的可移植序列化
【发布时间】:2023-09-15 22:44:01
【问题描述】:

我最近一直在研究一个需要存储和加载大量数据(包括单精度浮点值)的系统。我决定对整数的网络字节顺序进行标准化,并决定以大端格式存储浮点值,即:

  |-- Byte 0 --| |-- Byte 1 -|  Byte 2   Byte 3
  #      ####### #     ####### ######## ########
Sign     Exponent          Mantissa
 1b    8b, MSB first    23b, MSB first

理想情况下,我想提供htonl()ntohl() 之类的函数,因为我已经使用它们来抽取整数,而且我还想以一种尽可能独立于平台的方式来实现它(而假设 float 类型对应于 IEEE754 32 位浮点值)。有没有办法,可能使用ieee754.h,来做到这一点?

我有一个似乎可行的答案,我将在下面发布它,但它似乎非常缓慢且效率低下,我将不胜感激有关如何使其更快和/或更可靠的任何建议.

【问题讨论】:

  • 这个怎么样:*.com/a/2782742/1327576
  • 我看了那个答案,显然它取决于主机表示是小端的假设。我正在寻找与主机字节顺序无关的东西。
  • 可以说snprintf(b, sizeof(b), "%.9001f", yourvalue)(基于文本的表示)是最便携的。
  • 可以说!不幸的是,正如问题中提到的,我正在保存和加载大量数据。正如您所建议的那样,我从文本表示开始,但是对于数十亿个数据项,printfscanf 太慢了,并且生成的文件太大了。但是你指出这个选项是完全正确的。 :-)

标签: c floating-point portability ieee-754 endianness


【解决方案1】:

这是一个可移植的 IEEE 754 写入例程。 它将以 IEEE 754 格式写入一个 double,而不管主机上的浮点表示。

/*
* write a double to a stream in ieee754 format regardless of host
*  encoding.
*  x - number to write
*  fp - the stream
*  bigendian - set to write big bytes first, elee write litle bytes
*              first
*  Returns: 0 or EOF on error
*  Notes: different NaN types and negative zero not preserved.
*         if the number is too big to represent it will become infinity
*         if it is too small to represent it will become zero.
*/
static 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, but
    *there is no portable isnan() */
    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;
    }
    /* out of range. Set to infinity */
    else if (shift > 1023)
    {
        hilong = 1024 + ((1 << (expbits - 1)) - 1);
        hilong <<= (31 - expbits);
        hilong |= (sign << 31);
        lowlong = 0;
        goto writedata;
    }
    else
        fnorm = fnorm - 1.0; /* take the significant bit off mantissa */

    /* 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 (for convenience) */
    hibits = (long)(significand / 4294967296);
    hilong = (sign << 31) | (exp << (31 - expbits)) | hibits;
    x = significand - hibits * 4294967296;
    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);
}

【讨论】:

    【解决方案2】:

    简单得多,并且取决于与您相同的假设(即浮点和整数类型具有相同的字节顺序,并且几乎普遍有效-实际上您永远不会遇到系统不正确的地方):

    #include <string.h>
    
    float htonf(float val) {
        uint32_t rep;
        memcpy(&rep, &val, sizeof rep);
        rep = htonl(rep);
        memcpy(&val, &rep, sizeof rep);
        return val;
    }
    

    任何相当好的编译器都会优化掉两个memcpy 调用;它们的存在是为了打败过于急切的严格别名优化,因此最终与htonl 一样高效,加上单个函数调用的开销。

    【讨论】:

    • 是的,ieee754.h 假设——或者至少,我见过的所有实现都是如此(正如我所说,这在我们现代也可能是一个普遍有效的假设)。
    • 一些 ARM 架构不会有一些奇怪的混合端疯狂吗?无论如何,再次感谢您的回答 - 至少比我的更容易理解!
    • @Agent_L 该代码触发了未定义的行为。您正在取消引用指向不属于指针类型的值的指针。允许 C 编译器任意按照您的意图呈现这个 - 我实际看到发生的两件事是编译器用一些常量值(通常为零)代替负载,或者它用一个陷阱指令(即调用时的 insta-crash)。
    • @Agent_L:在某些平台上,ABI 要求浮点数比相同大小的整数更强(或更弱)对齐。在这样的平台上,取消引用类型双关指针很容易导致崩溃。
    • @Agent_L:我亲眼所见,导致我工作的库中出现错误。 GCC 4.0 将在适当的条件下将int32_t x = *(int32_t *)&amp;float; 优化为等效于int32_t x;(即x 将简单地未初始化)。说真的,请使用memcpy。它同样简单,明确保证是正确的,并且在使用合理的编译器时会生成相同的代码。有什么缺点?
    【解决方案3】:

    正如上面问题中提到的,我有一个解决我的问题的方法,但我并不特别喜欢它,我欢迎其他答案,所以我在这里而不是在问题中发布它。特别是,它似乎很慢,而且我不确定它是否会破坏严格的混叠以及其他潜在问题。

    #include <ieee754.h>
    
    float
    htonf (float val)
    {
      union ieee754_float u;
      float v;
      uint8_t *un = (uint8_t *) &v;
    
      u.f = val;
      un[0] = (u.ieee.negative << 7) + ((u.ieee.exponent & 0xfe) >> 1);
      un[1] = ((u.ieee.exponent & 0x01) << 7) + ((u.ieee.mantissa & 0x7f0000) >> 16);
      un[2] = (u.ieee.mantissa & 0xff00) >> 8;
      un[3] = (u.ieee.mantissa & 0xff);
      return v;
    }
    
    float
    ntohf (float val)
    {
      union ieee754_float u;
      uint8_t *un = (uint8_t *) &val;
    
      u.ieee.negative = (un[0] & 0x80) >> 7;
      u.ieee.exponent = (un[0] & 0x7f) << 1;
      u.ieee.exponent += (un[1] & 0x80) >> 7;
      u.ieee.mantissa = (un[1] & 0x7f) << 16;
      u.ieee.mantissa += un[2] << 8;
      u.ieee.mantissa += un[3];
    
      return u.f;
    }
    

    【讨论】:

    • 感谢您的信任投票。我有没有提到我正在处理 很多 数据? ;-) 是什么让你看起来很快?
    • 它不使用昂贵的操作,没有循环,没有跳转。而且我无法想象您如何能够以更少的操作摆脱困境。但我至少和你一样好奇,希望看到更好的建议。
    • 我的直接反应是这可能也很快。 “快速”是一个相对术语,所以我将澄清我的意思。我认为 CPU 将能够比网络传输数据快得多。即使忽略网络,转换速度也可能会超过主内存的带宽。
    • @JerryCoffin:即使你不能让网络传输更快,你也可以让处理器更快地完成任何给定的一批工作,让它回到低功耗状态并节省能源。
    最近更新 更多