C / C ++中double / floattypes的二进制序列化的可移植性
C ++标准没有讨论float和doubletypes的底层布局,只讨论了它们应该表示的值的范围。 (这对签名types也是如此,这是两个赞美还是别的什么)
我的问题是:什么是技术用于序列化/反序列化PODtypes,如double和float以便携方式? 目前看起来唯一的方法就是让字面值代表(如“123.456”),double的ieee754布局在所有体系结构上都不是标准的。
Brian“Beej Jorgensen”Hall在他的“networking编程指南”(Guide to Network Programming)中给出了一些将float
( double
)包含到uint32_t
(或uint64_t
)的代码,以便能够在两台机器之间通过networking安全地传输数据表示。 它有一些限制,主要是不支持NaN和无穷大。
这是他的包装function:
#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; }
人类可读的格式有什么问题。
与二进制相比,它有两个优点:
- 它是可读的
- 它是便携式的
- 它使支持真的很容易
(因为你可以要求用户用他们最喜欢的编辑器来看它) - 这很容易修复
(或在错误情况下手动调整文件)
坏处:
- 这不是紧凑的
如果这是一个真正的问题,你总是可以压缩它。 - 提取/生成可能会稍微慢一些
注意一个二进制格式可能也需要被标准化(参见htonl()
)
要以全精度输出双精度型:
double v = 2.20; std::cout << std::setprecision(std::numeric_limits<double>::digits) << v;
好。 我不相信这是确切的。 它可能会失去精度。
只需将二进制IEEE754表示forms写入磁盘,并将其logging为存储格式(以及字节序)。 然后,如果有必要的话,就可以将其转换为内部表示。
看一下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链接
创build一个适当的串行器/解串器接口来写/读这个。
接口可以有几个实现,你可以testing你的选项。
如前所述,显而易见的select是:
- IEEE754,如果体系结构直接支持,则写入/读取二进制块;如果体系结构不支持,则parsing该块
- 文本:总是需要parsing。
- 无论你还能想到什么
只要记住 – 一旦你有这个层,如果你只支持在内部使用这种格式的平台,你总是可以从IEEE754开始。 这样,只有当你需要支持不同的平台时,你才会有额外的努力! 不要做不必要的工作。
你应该把它们转换成一种你永远可以用来重新创build你的浮动/双打的格式。
这可以使用string表示forms,或者,如果您需要占用较less空间的内容,请使用ieee754(或您select的任何其他格式)表示您的数字,然后像使用string一样对其进行parsing 。
我认为答案“取决于”你的特定应用程序和它的性能概况是什么。
假设你有一个低延迟的市场数据环境,那么使用string是非常愚蠢的。 如果你所传递的信息是价格,那么双打(和它们的二进制表示)真的很难处理。 如果你真的不关心性能,你想要的是可见性(存储,传输),那么string是一个理想的select。
我实际上会select浮点数/双精度的整数尾数/指数表示 – 即在最早的机会下,将float / double转换为一对整数,然后传送。 然后你只需要担心整数的可移植性,以及各种例程(例如hton()
例程)来处理转换。 还要将所有内容存储在最stream行的平台上(例如,如果您只使用linux,那么以big endian存储内容有什么意义?)
SQLite4使用新的格式来存储双打和浮动
- 即使在缺乏IEEE 754二进制64位浮点数支持的平台上,它也可以可靠且一致地工作。
- 货币计算通常可以精确地完成而不需要四舍五入。
- 任何有符号或无符号的64位整数都可以精确表示。
- 浮点范围和精度超过IEEE 754二进制64位浮点数。
- 正负无穷和NaN(非数)具有明确的表示。
资料来源: