4 个回答
string-double 转换,这个问题非常复杂。以至于 gcc,glibc,VC,Java,PHP 等人们熟知的语言/运行时库都曾经写出过 bug [1] ,要么导致程序 crash,要么是结果有错误。
之前我在开发一种 JSON-like 的数据交换格式 [2] 的 C++ 实现 [3] 时,基于不可避免的原因,曾在这个库中内置了 string-double 转换 [4] 的功能。当时花了很多时间,参考了很多资料和实现,也做了很多 test。
string-double 有多种不同的算法,但有一种基于大整数的算法 [5] 使用广泛。虽然速度较慢,但有正确性的保证。这里无法展开所有代码层面的实现细节,只是简单地阐述一下它的过程。
例如,要把 1.2345678901234567e22 从 string literal 转换到 double 精度浮点数,
首先,将它表达为整数乘以 10 的 n 次幂:
1.2345678901234567e22 = 12345678901234567 x 10^6
也就是
12345678901234567 x 1000000 = 12345678901234567000000
然后转为分数表达。由于正指数将它实际变成了整数,所以分母取 1,也就是
12345678901234567000000 / 1
接着,为了将它缩放到 [2^52, 2^53) 这个范围内(请考虑 double 的有效位数),将分母乘以 2^21,
12345678901234567000000 / 2^21 = 12345678901234567000000 / 2097152
做除法后得到
12345678901234567000000 / 2097152 = 5886878443352969 余 1355712
由于 1355712 超过了分母的 1/2,根据 IEEE 标准,将它做一个 round-up,得到
5886878443352970
以二进制表达为
10100111010100001010110110010011100111011001110001010
由于前面做了缩放,分母乘以 2^21 了,所以这里要做一个逆操作,将它乘以 2^21
10100111010100001010110110010011100111011001110001010 x 2^21
归一化,
1.010011101010000101011011001001110011101100111000101 x 2^21 x 2^52
得到
1.010011101010000101011011001001110011101100111000101 x 2^73
最后,将它以 IEEE Binary64 编码:
0100010010000100111010100001010110110010011100111011001110001010
- 最前面的符号位是 0,代表正数
- 接着的 exponent 为 10001001000,十进制为 1096,由 73 + 1023(exponent bias)得到
- 后面 fraction 为 010011101010000101011011001001110011101100111000101
- fraction 为之前归一化得到的二进制的 1. 后的部分,这是因为浮点数中最前面的 1 是暗含的。所以它的十进制 1383278815982474 来自 5886878443352970 - 2^52
如果想要了解更多细节,可以从这里开始:
参考
- ^ https://www.exploringbinary.com/topics/#correctly-rounded-decimal-to-floating-point
- ^ https://www.eclog.org/
- ^ https://github.com/Vallest/Eclog-CPP
- ^ https://github.com/Vallest/Eclog-CPP/blob/main/Source/DoubleConversion.cpp#L221
- ^ https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/