【问题标题】:Portability of binary serialization of double/float type in C++C++中double/float类型二进制序列化的可移植性
【发布时间】:2011-06-11 14:44:18
【问题描述】:

C++ 标准没有讨论 float 和 double 类型的底层布局,只讨论它们应该表示的值的范围。 (这对于有符号类型也是如此,是双的恭维还是别的什么)

我的问题是:以可移植方式序列化/反序列化 POD 类型(例如 double 和 float)的技术是什么?目前,这样做的唯一方法似乎是让值按字面意思表示(如“123.456”),双精度的 ieee754 布局并不是所有架构的标准。

【问题讨论】:

  • 如果您需要文件存储,HDF5 或 NetCDF 有很大帮助。

标签: c++ serialization double portability ieee-754


【解决方案1】:

找到这个旧线程。缺少一种解决相当多情况的解决方案 - 使用固定点,在任一端使用内置转换传递具有已知比例因子的整数。因此,您根本不必为底层的浮点表示而烦恼。

当然也有缺点。此解决方案假定您可以拥有一个固定的比例因子,并且仍然可以获得特定应用程序所需的范围和分辨率。此外,您在序列化结束时将浮点转换为定点,然后在反序列化时转换回来,这会引入两个舍入误差。 然而,多年来我发现固定点几乎在所有情况下都足以满足我的需求,而且速度也相当快。

固定点的典型案例是嵌入式系统或其他设备的通信协议。

【讨论】:

    【解决方案2】:

    SQLite4 使用一种新格式来存储双精度和浮点数

    • 即使在不支持 IEEE 754 binary64 浮点数的平台上,它也能可靠且一致地工作。
    • 货币计算通常可以精确地完成,无需四舍五入。
    • 任何有符号或无符号的 64 位整数都可以精确表示。
    • 浮点范围和精度超过 IEEE 754 binary64 浮点数。
    • 正负无穷和 NaN(非数字)具有明确定义的表示。

    来源:

    https://sqlite.org/src4/doc/trunk/www/design.wiki

    https://sqlite.org/src4/doc/trunk/www/decimal.wiki

    【讨论】:

      【解决方案3】:

      人类可读的格式有什么问题。

      与二进制相比,它有几个优点:

      • 可读
      • 便携
      • 它使支持变得非常容易
        (因为您可以要求用户在他们最喜欢的编辑器中查看它甚至单词)
      • 很容易修复
        (或在错误情况下手动调整文件)

      缺点:

      • 不紧凑
        如果这是一个真正的问题,您可以随时压缩它。
      • 提取/生成可能会稍慢
        请注意,二进制格式可能也需要规范化(请参阅htonl()

      以全精度输出双精度:

      double v = 2.20;
      std::cout << std::setprecision(std::numeric_limits<double>::digits) << v;
      

      好的。我不相信这是完全准确的。它可能会丢失精度。

      【讨论】:

      • 附加缺点:不精确。这一点的重要性可能因应用程序而异。
      • +1 即使可能存在其他缺点:生成/解析成本更高——只会影响主要读取/写入数据的应用程序的性能,但仍然如此。尺寸确实会影响那里,并且 zip-ping 甚至会使性能变得更糟......不过,在 99.9% 的情况下,几乎所有现实世界的情况都是一个很好的解决方案。
      • @Martin:文字表示解码非常慢,我正在开发一个处理非常大的时间序列的系统,并且紧凑、精确和高速的可解码表示是必须的 - 可移植性是也很重要。
      • @Martin:嗯。我认为我从未见过可以配置为写出浮点数的所有精度的格式化函数。如果存在,那么当然没有损失。所以我的担忧与“它不紧凑”的缺点有关:你最终会在合理大小的表示和精确的表示之间进行权衡。 (同样,这两种方法的重要性因应用程序而异)
      • @Maxim:所以你的意思是它不能在 Windows 或当前的 C++ 标准上工作。
      【解决方案4】:

      查看 glib 2 中的(旧)gtypes.h 文件实现 - 它包括以下内容:

      #if G_BYTE_ORDER == G_LITTLE_ENDIAN
      union _GFloatIEEE754
      {
        gfloat v_float;
        struct {
          guint mantissa : 23;
          guint biased_exponent : 8;
          guint sign : 1;
        } mpn;
      };
      union _GDoubleIEEE754
      {
        gdouble v_double;
        struct {
          guint mantissa_low : 32;
          guint mantissa_high : 20;
          guint biased_exponent : 11;
          guint sign : 1;
        } mpn;
      };
      #elif G_BYTE_ORDER == G_BIG_ENDIAN
      union _GFloatIEEE754
      {
        gfloat v_float;
        struct {
          guint sign : 1;
          guint biased_exponent : 8;
          guint mantissa : 23;
        } mpn;
      };
      union _GDoubleIEEE754
      {
        gdouble v_double;
        struct {
          guint sign : 1;
          guint biased_exponent : 11;
          guint mantissa_high : 20;
          guint mantissa_low : 32;
        } mpn;
      };
      #else /* !G_LITTLE_ENDIAN && !G_BIG_ENDIAN */
      #error unknown ENDIAN type
      #endif /* !G_LITTLE_ENDIAN && !G_BIG_ENDIAN */
      

      glib link

      【讨论】:

        【解决方案5】:

        我认为答案“取决于”您的特定应用程序及其性能概况。

        假设您有一个低延迟的市场数据环境,那么使用字符串坦率地说是愚蠢的。如果您要传达的信息是价格,那么双精度数(以及它们的二进制表示)确实很难使用。如果您并不真正关心性能,而您想要的是可见性(存储、传输),那么字符串是理想的选择。

        我实际上会选择浮点数/双精度数的整数尾数/指数表示 - 即尽早将浮点数/双精度数转换为一对整数,然后将其传输。然后,您只需担心整数的可移植性以及各种例程(例如为您处理转换的hton() 例程)。还将所有内容存储在您最流行的平台的字节序中(例如,如果您只使用 linux,那么以大字节序存储内容有什么意义?)

        【讨论】:

        • 市场数据是一个不好的例子:检索市场数据通常比解析一堆字符串更昂贵。这取决于您的技术,但通常此类内容存储在数据库中。
        • @Alex,嗯?我想你可能误解了我,当我谈论低延迟环境时,我不是在谈论历史数据——可能在数据库中,而是在每一微秒都很重要的交易环境中——你真的想要在字符串转换例程中添加额外的延迟? atoi(), scanf(), sprintf(), 比较慢的...
        • 我认为你应该购买更快的硬件(即更快的内存)。字符串处理在 CPU 方面非常快,比从内存中获取字符串要快得多...
        • @Alex,哈哈...您可以在问题上投入更多硬件,但它不会消失,您只是延迟不可避免的...所以,如果您不处理字符串,那么你就不必去取它了,我会说那是一个巨大的节省...... ;)
        • 在许多系统上将字符串转换为双精度数比使用双精度数进行算术运算要慢数百倍。如果你正处于计算可行和不可行的边缘,使用字符串表示可能很容易把你推倒。
        【解决方案6】:

        只需将二进制 IEEE754 表示写入磁盘,并将其记录为您的存储格式(连同 is endianness)。然后由实现在必要时将其转换为其内部表示。

        【讨论】:

          【解决方案7】:

          Brian "Beej Jorgensen" Hall 在他的Guide to Network Programming 中提供了一些代码来打包float(或double)到uint32_t(或uint64_t),以便能够通过网络安全地传输它两台机器之间可能并不都同意他们的代表。它有一些限制,主要是它不支持 NaN 和无穷大。

          这是他的打包函数:

          #define pack754_32(f) (pack754((f), 32, 8))
          #define pack754_64(f) (pack754((f), 64, 11))
          
          uint64_t pack754(long double f, unsigned bits, unsigned expbits)
          {
              long double fnorm;
              int shift;
              long long sign, exp, significand;
              unsigned significandbits = bits - expbits - 1; // -1 for sign bit
          
              if (f == 0.0) return 0; // get this special case out of the way
          
              // check sign and begin normalization
              if (f < 0) { sign = 1; fnorm = -f; }
              else { sign = 0; fnorm = f; }
          
              // 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--; }
              fnorm = fnorm - 1.0;
          
              // calculate the binary form (non-float) of the significand data
              significand = fnorm * ((1LL<<significandbits) + 0.5f);
          
              // get the biased exponent
              exp = shift + ((1<<(expbits-1)) - 1); // shift + bias
          
              // return the final answer
              return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
          }
          

          【讨论】:

          • 如果需要,包含 NaN、无穷大和非规范化数字应该不难。此外,这段代码是公共领域的,这使它成为一个很好的答案。
          • 基于frexp 的方法会始终比重复的浮点除法/乘法更快吗? frexp 在一次通话中为您提供expfnorm。请记住,IEEE 754 double 具有 11 位的指数,因此您可以将 2 除/乘以数百次。
          • @jw013 在这种情况下,基于frexp 的方法会是什么样子?我现在正在为浮点序列化而苦苦挣扎,虽然frexp 方法看起来很有趣,但我不知道如何将尾数(介于 0.5 和 1 之间)转换为代表有效位的一系列位IEEE 浮点数或双精度数。有没有一种高效且便携的方法来做到这一点?
          • 有人可以向我解释significand = fnorm * ((1LL&lt;&lt;significandbits) + 0.5f);这是如何工作的吗?
          【解决方案8】:

          创建一个适当的序列化器/反序列化器接口用于写入/读取。

          然后接口可以有多个实现,您可以测试您的选项。

          如前所述,显而易见的选择是:

          • IEEE754 如果架构直接支持则写入/读取二进制块,如果架构不支持则解析它
          • 文本:总是需要解析。
          • 你还能想到什么。

          请记住——一旦你有了这个层,如果你只支持在内部使用这种格式的平台,你总是可以从 IEEE754 开始。这样,只有当您需要支持不同的平台时,您才需要付出额外的努力!不要做不必要的工作。

          【讨论】:

            【解决方案9】:

            您应该将它们转换为您始终能够使用的格式,以便重新创建您的浮点数/双精度数。

            这可以使用字符串表示,或者,如果您需要占用更少空间的东西,可以用 ieee754(或您选择的任何其他格式)表示您的号码,然后像使用字符串。

            【讨论】:

            • 是否有任何库可以采用双精度并转换为特定的二进制格式?目前我们所做的只是将内存中的布局写入磁盘,这没问题,但在异构环境中它不会很好地工作。
            • 我猜有一些,但我不知道,抱歉。
            猜你喜欢
            • 2012-07-15
            • 1970-01-01
            • 1970-01-01
            • 2016-05-19
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多