最近碰到Lua语言中的浮点数比较问题,如下:
print(9007199254740991 + 0.0 == 9007199254740991)
print(9007199254740992 + 0.0 == 9007199254740992)
print(9007199254740993 + 0.0 == 9007199254740993)
上面的代码输出为:
true
true
false
将下面类似的代码改写为C/C++中的代码:
#include
#include
int main() {
std::cout << std::boolalpha;
std::cout<< (9007199254740991 + 0.0 == 9007199254740991) << std::endl;
std::cout<< (9007199254740992 + 0.0 == 9007199254740992) << std::endl;
std::cout<< (9007199254740993 + 0.0 == 9007199254740993) << std::endl;
}
输出为
true
true
true
为什么Lua中第三个输出是false
,为什么C++给出的输出都是true
呢?本文结合IEEE-754中关于浮点数的表示机制,分析上述结果出现的原因,同时给出C/C++浮点数进行比较应该注意的事项。
IEEE-754浮点数中涉及到32, 64, 128位浮点数表示机制,它们的原理类似, n n n位的比特串中第一位是符号位,中间是指数位,后面是小数位,以单精度、双精度,四精度如下表1:
符号位(sign/s) | 指数位(exponent/exp) | 小数位(fraction/frac) | |
---|---|---|---|
Single precision | 31 | 30->23 (8bit) | 22->0 (23bit) |
Double precision | 63 | 62->52 (11bit) | 51->0 (52bit) |
Quadruple precision | 127 | 126->112 (15bit) | 111->0 (112bit) |
浮点数的值由下面的公式1进行计算:
V = ( − 1 ) s × 2 E × M V = (−1)^s × 2^E × M V=(−1)s×2E×M
根据 e x p exp exp的编码是否为全0,全1,以及不为全0或全1,浮点数可以分为三类:
对于规格化和非规格化的浮点数,对于公式 V = ( − 1 ) s × 2 E × M V = (−1)^s × 2^E × M V=(−1)s×2E×M,其计算 M M M和 E E E的过程如下表2所示:
b i a s bias bias | E E E | E m i n E_{min} Emin | E m a x E_{max} Emax | M M M | |
---|---|---|---|---|---|
规格化浮点数 | 2 k − 1 − 1 2^{k-1}-1 2k−1−1 | e − b i a s {e-bias} e−bias | 2 − 2 k − 1 {2-2^{k-1}} 2−2k−1 | 2 k − 1 − 1 2^{k-1}-1 2k−1−1 | 1 + f {1 + f} 1+f |
非规格化的浮点数 | 2 k − 1 − 1 2^{k-1}-1 2k−1−1 | 1 − b i a s {1-bias} 1−bias | 2 − 2 k − 1 {2-2^{k-1}} 2−2k−1 | 2 − 2 k − 1 {2-2^{k-1}} 2−2k−1 | f {f} f |
假设8位比特串来表示一个浮点数, k = 4 , n = 3 k=4, n = 3 k=4,n=3, 那么 b i a s = 7 bias = 7 bias=7,我们有下面的表3:
描述 | 位表示 | E E E | M M M | V V V | 十进制值 |
---|---|---|---|---|---|
无穷小 | 1 1111 000 | … | … | … | -inf |
0 | 0 0000 000 | -6 | 0 | 0 | 0.0 |
最小非规格化数 | 0 0000 001 | -6 | 1 8 \frac{1}{8} 81 | 1 512 \frac{1}{512} 5121 | 0.001953 |
… | … | … | … | … | … |
最大非规格化数 | 0 0000 111 | -6 | 7 8 \frac{7}{8} 87 | 7 512 \frac{7}{512} 5127 | 0.013672 |
最小规格化数 | 0 0001 000 | -6 | 8 8 \frac{8}{8} 88 | 8 512 \frac{8}{512} 5128 | 0.015625 |
… | … | … | … | … | … |
最大规格化数 | 0 1110 111 | 7 | 15 8 \frac{15}{8} 815 | 1920 8 \frac{1920}{8} 81920 | 240 |
无穷大 | 0 1111 000 | … | … | … | inf |
观察上面的表格,可以发现:
我们来看整数12345和单精度浮点数12345.0的表示方法:
12345的二进制数0000 0000 0000 0000 0011 0000 0011 1001
对应的浮点数位表示是: 1.100000011100 1 2 ∗ 2 13 1.1 0000 0011 1001_2 * 2 ^{13} 1.10000001110012∗213, 那么由 2 k − 1 − 1 = 2 7 − 1 = 127 2^{k-1}-1 = 2^7 - 1 = 127 2k−1−1=27−1=127 ,
e − 127 = 13 e - 127 = 13 e−127=13,得到 e = 140 e = 140 e=140,其二进制表示为1000 1100,加上符号位0,我们可以得到:
12345的单精度浮点数表示为:0 1000 1100 1000 0001 1100 1000 0000 000
对比上面加粗部分,我们发现它们是一样的,也就是说,12345这个整数是能够被单精度浮点数精确表示的。
结合3的例子,对于一个采用n个bit来表示小数部分的浮点数,它不能精确表示的最小的正整数该怎么计算呢?这里我们假设它的 k k k个指数位能够表示的数值足够大,使得 E m i n < = n < = E m a x E_{min}<=n<=E_{max} Emin<=n<=Emax,这在一般情况下都是成立的。
考虑下面的式子, 由于 f = f n − 1 . . . f 1 f 0 f=f_{n-1}...f_1f_0 f=fn−1...f1f0,结合公式1和表2: M = 1 + f M={1 + f} M=1+f:
V = 2 n ∗ ( 1 + f n − 1 / 2 + f n − 2 / 2 2 + . . . f 0 / 2 n ) , f i = 0 ∣ f i = 1 , V = 2^n*(1 + f_{n-1}/2 + f_{n-2}/2^2 +... f_0/2^{n}), f_i = 0 | f_i = 1, V=2n∗(1+fn−1/2+fn−2/22+...f0/2n),fi=0∣fi=1,
那么,其取值范围是 2 n < = v < = 2 n + 1 − 1 2^n <=v <=2^{n+1} - 1 2n<=v<=2n+1−1, 同时,对于 2 n + 1 2^{n+1} 2n+1, 小数部分如果全为0, 如果指数部分可以表示 n + 1 n+1 n+1, 那么, 2 n + 1 2^{n+1} 2n+1也是能被浮点数精确表示。那么采用n个bit来表示小数部分的浮点数不能精确表示的最小正整数就是 2 n + 1 + 1 2^{n+1} + 1 2n+1+1
当然, 对于 2 n + 2 2^{n+2} 2n+2, 只要指数部分可以表示 n + 2 {n+2} n+2, 小数部分全为0, 该正整数也是可以被精确表示的。
在Lua 5.2以及以前的的版本中,所有的数值都采用double-precision floating-point来表示,也就是64位bit来表示整数或数,由表1,用来表示双精度浮点数的小数部分有52个bit, 根据问题4的结论,Lua的双精度浮点数可以连续准确表示的整数范围是 [ − 2 53 , 2 53 ] [-2^{53}, 2^{53}] [−253,253],即 [ − 9007199254740992 , 9007199254740992 ] [-9007199254740992, 9007199254740992] [−9007199254740992,9007199254740992]
从Lua 5.3开始, Lua 添加了interger类型:用64bit来表示一个有符号的int, 其表示范围为 [ − 9223372036854775808 ( 0 x 8000000000000000 ) , 9223372036854775807 ( 0 x 7 f f f f f f f f f f f f f f f ) ] [-9223372036854775808(0x8000000000000000), 9223372036854775807(0x7fffffffffffffff)] [−9223372036854775808(0x8000000000000000),9223372036854775807(0x7fffffffffffffff)]
double-precision floating-point和5.2一样,只不过主要用来表示双精度浮点数。
在Lua中,当一个整数和浮点数相加时,会将该整数提升为浮点数,回到最初的问题:
print(9007199254740991 + 0.0 == 9007199254740991)
print(9007199254740992 + 0.0 == 9007199254740992)
print(9007199254740993 + 0.0 == 9007199254740993)
第三行的左侧由于会提升到浮点数,但9007199254740993在浮点数中没有精确表示,只能近似表示为9007199254740992, 而右侧是整型,可以精确表示,Lua中数值比较始终比较的是其算术值,也就是实际数值,所以第三行输出为false
。
前面说过,对于对于 2 n + 2 2^{n+2} 2n+2, 只要指数部分可以表示 n + 2 {n+2} n+2, 小数部分全为0, 该正整数也是可以被精确表示的。
我们在Lua 中验证这一点, 这里 n = 52 , n + 2 = 54 n=52, {n+2=54} n=52,n+2=54:
print(2^54 | 0)
print(18014398509481984 + 0.0 == 18014398509481984)
输出为:
18014398509481984
true
C/C++中,同一句语句或表达式如果使用了多种类型的变量和常量(类型混用),C会自动把它们转换成同一种类型。以下是自动类型转换的基本规则:
在表达式中,char 和 short 类型的值,无论有符号还是无符号,都会自动转换成 int 或者 unsigned int(如果 short 的大小和 int 一样,unsigned short 的表示范围就大于 int,在这种情况下,unsigned short 被转换成 unsigned int)。因为它们被转换成表示范围更大的类型,故而把这种转换称为“升级(promotion)”。
按照从高到低的顺序给各种数据类型分等级,依次为:long double, double, float, unsigned long long, long long, unsigned long, long, unsigned int 和 int。这里有一个小小的例外,如果 long 和 int 大小相同,则 unsigned int 的等级应位于 long 之上。char 和 short 并没有出现于这个等级列表,是因为它们应该已经被升级成了 int 或者 unsigned int。
#include
#include
int main() {
std::cout << std::boolalpha;
std::cout<< (9007199254740991 + 0.0 == 9007199254740991) << std::endl;
std::cout<< (9007199254740992 + 0.0 == 9007199254740992) << std::endl;
std::cout<< (9007199254740993 + 0.0 == 9007199254740993) << std::endl;
}
等号两边都升级为double类型, 其数值一样,表示一样,因而第三行输出true
。