我正在阅读有关网络编程的指南,我非常喜欢:https://beej.us/guide/bgnet/html/split/slightly-advanced-techniques.html#serialization
不过,我对一些事情感到困惑。在关于序列化的这一节中,他谈到了出于字节排序的原因而序列化整数,这对我来说很有意义,但他还包括这两个函数 pack754 和 unpack754,用于以 IEEE-754 格式序列化浮点数。
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;
}
long double unpack754(uint64_t i, unsigned bits, unsigned expbits)
{
long double result;
long long shift;
unsigned bias;
unsigned significandbits = bits - expbits - 1; // -1 for sign bit
if (i == 0) return 0.0;
// pull the significand
result = (i&((1LL<<significandbits)-1)); // mask
result /= (1LL<<significandbits); // convert back to float
result += 1.0f; // add the one back on
// deal with the exponent
bias = (1<<(expbits-1)) - 1;
shift = ((i>>significandbits)&((1LL<<expbits)-1)) - bias;
while(shift > 0) { result *= 2.0; shift--; }
while(shift < 0) { result /= 2.0; shift++; }
// sign it
result *= (i>>(bits-1))&1? -1.0: 1.0;
return result;
}
我感到困惑的是,这些函数的工作原理是先查看符号的第一位,然后查看指数的下一个 X 位,再查看尾数的下一个 Y 位。那么,这是否意味着浮点数在主机上必须已经采用 IEEE-754 格式才能正常工作?
这只是为了解释格式,还是你在现实生活中实际会做的事情?
是的。FP 编码在不同的实现中有很多变化,包括大小、字节序、精度、指数范围、次正规支持(甚至可能是基数)。
不,即使
long double
不是 IEEE,打包/解包也会“工作”(参见以下问题)。看起来像是学习者代码。我不会使用提供的打包/解包代码,因为它有缺点(见下文),尤其是 2 个非常低效的循环。循环可能会使用binary128
while
进行数千次迭代。该代码是一个漏洞百出的尝试,试图将任意编码打包
long double
成 IEEE binary64。它无法很好地处理接近 0.0 的值、舍入、处理溢出和无穷大/NAN。pack754()
至少存在以下这些缺点:if (f == 0.0) return 0;
序列化过程中会丢失信息,因为它对 +0.0 和 -0.0 都返回 0。测试 FP 符号位时,不要使用if (f < 0)
,而是使用 ,即使为零或 NANif (signbit(f))
也能很好地提取符号位。f
long double
可能超过 64 位,因此uint64_t pack754(long double f, unsigned bits, unsigned expbits)
在尝试打包成 64 位时会丢失信息。我想 OP 正在容忍这种信息丢失。1LL<<significandbits
溢出()时为 UBsignificandbits >= 63
。1ULL<<significandbits
具有一些优势,但是溢出(significandbits >= 64
)仍然是一个问题。float
将数学与后期的数学一起使用long double
是短视的。((1LL<<significandbits) + 0.5L)
更有意义一点。与
while(fnorm >= 2.0)
代码不同,使用long double frexpl(long double value, int *p)
来提取规范化的值和指数。使用long double ldexpl(long double x, int p)
来重新组合。while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
当无穷大时,可能会出现无限循环fnorm
。+ 0.5f
因为圆角有很多问题。最好使用lround()
和朋友。...
对于简单的跨平台 FP 值交换,我认为
sprintf(buf, "%La", x)
第一步是打包和解strtold()
包。将 FP 封装到紧凑的空间中
intN_t
并在许多计算机实现中保持精度/范围的忠实性是相互竞争的目标。哪个更重要:忠实转换还是小数据包大小?
我使用过的大多数系统都重视忠实转换而不是小数据包大小。
为了便于移植而将打包
long double
到 64 位中只是一个不明智的设计。