Lua和C++中的浮点数的比较

文章目录

  • 问题
  • 一、IEEE-754浮点数表示机制
    • 1. 表示机制
    • 2. 优缺点分析
    • 3. 例子
    • 4. 问题
  • 二、Lua中的浮点数表示
    • 1. Lua 中的整型和浮点型
    • 2. Lua中的数值比较
  • 三、C/C++中的自动类型转换
  • 总结


问题

最近碰到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浮点数表示机制

1. 表示机制

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

下面给出了单精度和双精度浮点数的表示示意图1
Lua和C++中的浮点数的比较_第1张图片

图1(来源:csapp: fig:2.32)

浮点数的值由下面的公式1进行计算:
V = ( − 1 ) s × 2 E × M V = (−1)^s × 2^E × M V=(1)s×2E×M

公式1
  • 第一个符号位 s s s为0时,表示正数,为1时,表示负数
  • 接下来的 k k k-bit表示指数 E E E, e = e k − 1 . . . e 1 e 0 e=e_{k-1}...e_1e_0 e=ek1...e1e0, E E E e x p exp exp( k k k-bit转换为无符号整数)间接表示,但不一定等于 e e e
  • 最后的 n n n-bit表示小数 M M M, f = f n − 1 . . . f 1 f 0 f=f_{n-1}...f_1f_0 f=fn1...f1f0, M M M f r a c frac frac( n n n-bit转换为小数)间接表示,但不一定等于 f f f

根据 e x p exp exp的编码是否为全0,全1,以及不为全0或全1,浮点数可以分为三类:

  1. 规格化的浮点数: e x p exp exp的编码不为全0或全1
  2. 非规格化的浮点数: e x p exp exp的编码为全0
  3. 特殊值: e x p exp exp的编码为全1
    • 如果 f r a c frac frac位全为0,表示无穷大( s s s为0为正无穷大, s s s为1为负无穷大)
    • 如果 f r a c frac frac位不全为0,表示NaN(Not a Number)

下面以单精度浮点数为例给出了上面三类浮点数的表示如下图2:Lua和C++中的浮点数的比较_第2张图片

图2(来源:csapp: fig:2.33)

对于规格化和非规格化的浮点数,对于公式 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 2k11 e − b i a s {e-bias} ebias 2 − 2 k − 1 {2-2^{k-1}} 22k1 2 k − 1 − 1 2^{k-1}-1 2k11 1 + f {1 + f} 1+f
非规格化的浮点数 2 k − 1 − 1 2^{k-1}-1 2k11 1 − b i a s {1-bias} 1bias 2 − 2 k − 1 {2-2^{k-1}} 22k1 2 − 2 k − 1 {2-2^{k-1}} 22k1 f {f} f
表2

2. 优缺点分析

假设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
表3

观察上面的表格,可以发现:

  • 最大非规格化数和最小规格化数之间是平滑切换的
  • 正数和负数是对称的,由符号位决定
  • 越靠近0,表示越精确,越远离0,表示越稀疏,越粗略

3. 例子

我们来看整数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.10000001110012213, 那么由 2 k − 1 − 1 = 2 7 − 1 = 127 2^{k-1}-1 = 2^7 - 1 = 127 2k11=271=127 ,
e − 127 = 13 e - 127 = 13 e127=13,得到 e = 140 e = 140 e=140,其二进制表示为1000 1100,加上符号位0,我们可以得到:
12345的单精度浮点数表示为:0 1000 1100 1000 0001 1100 1000 0000 000

对比上面加粗部分,我们发现它们是一样的,也就是说,12345这个整数是能够被单精度浮点数精确表示的。

4. 问题

结合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=fn1...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+fn1/2+fn2/22+...f0/2n),fi=0∣fi=1,
那么,其取值范围是 2 n < = v < = 2 n + 1 − 1 2^n <=v <=2^{n+1} - 1 2n<=v<=2n+11, 同时,对于 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中的浮点数表示

1. Lua 中的整型和浮点型

在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一样,只不过主要用来表示双精度浮点数。

2. Lua中的数值比较

在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/C++中,同一句语句或表达式如果使用了多种类型的变量和常量(类型混用),C会自动把它们转换成同一种类型。以下是自动类型转换的基本规则:

  1. 在表达式中,char 和 short 类型的值,无论有符号还是无符号,都会自动转换成 int 或者 unsigned int(如果 short 的大小和 int 一样,unsigned short 的表示范围就大于 int,在这种情况下,unsigned short 被转换成 unsigned int)。因为它们被转换成表示范围更大的类型,故而把这种转换称为“升级(promotion)”。

  2. 按照从高到低的顺序给各种数据类型分等级,依次为: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

总结

  1. Lua中数值的比较是根据其算术值进行比较,判断 double == integer时,会根据double的实际二进制表示计算出其算术值,从而比较两边的算术值是否相等。由于浮点数的二进制表示在表示实数时并非一一对应,就会出现上面的问题。
  2. C/C++中由于类型提升,两边均提升到同样的类型,因而比较结果总是相等的。

你可能感兴趣的:(C++,c++,lua,浮点数)