0.1 + 0.2 == 0.3 吗?

1. 背景

这要从一段 golang 代码讲起:

func main() {
    var a float32 = 0.1
    var b float32 = 0.2
    var c float32 = 0.3
    fmt.Println(a + b == c)     // true
    
    var aa float64 = 0.1
    var bb float64 = 0.2
    var cc float64 = 0.3
    fmt.Println(aa + bb == cc)  // false

	fmt.Println(0.1+0.2 == 0.3) // true
}

为什么同样是 0.1 + 0.2,有时候等于 0.3 有时候又不等呢?本文这就为您揭秘。

2. 浮点数的计算机表示

浮点数在计算机中一般采用 IEEE754 表示法存储,相关细节可以在维基百科找到,读者可以自行阅读。本文只关注其根本表示形式,即 符号位 + 指数位(阶码) + 尾码
在这里插入图片描述
V 表示小数数值;S 表示符号位;M 表示尾码;E 表示指数位(阶码)

在内存中的存储形式如下图:
在这里插入图片描述

3. 十进制小数转二进制表示

可以使用乘 2 取整法,例如:

2 * 0.1 = 0.2  整数位0
2 * 0.2 = 0.4  整数位0
2 * 0.4 = 0.8  整数位0
2 * 0.8 = 1.6  整数位1
2 * 0.6 = 1.2  整数位1
2 * 0.2 = 0.4  整数位0
2 * 0.4 = 0.8  整数位0
2 * 0.8 = 1.6  整数位1
⋯⋯

那么,0.1 的二进制表示即为: 0.00011001100110011…,转换为指数表示形式,即为:1.1001100110011… * 2^(-4)

4. float32 类型的 0.1 + 0.2 运算

float32 类型的小数包含:1 位符号位,8 位阶码,23 位尾码,使用 “乘 2 取整法” 分别计算 0.1、0.2、0.3 的二进制指数表示形式:

0.1 => 1.10011001100110011001101 * 2^(-4) (小数位第 24 位为 1,向前进位)
0.2 => 1.10011001100110011001101 * 2^(-3) (小数位第 24 位为 1,向前进位)
0.3 => 1.00110011001100110011010 * 2^(-2) (小数位第 24 位为 1,向前连续进位)

计算 0.1 + 0.2,由于阶码不同,需要首先对齐阶码,规则是向大阶码对齐,因此 0.1 的阶码需要由 -4 变为 -3,即:

0.1 => 0.11001100110011001100111 * 2^(-3)  (原来最右边的一位被舍弃,但会进位)
0.2 => 1.10011001100110011001101 * 2^(-3)  (対阶前后保持不变)

根据上述表示方法,计算二进制加法 0.1 + 0.2 的结果为:

0.1 + 0.2 = 10.01100110011001100110100 * 2^(-3) = 1.00110011001100110011010 * 2^(-2)
0.3 => 1.00110011001100110011010 * 2^(-2)

由于 0.1 + 0.2 的结果在计算机底层存储与 0.3 的底层存储一致,因此 0.1 + 0.2 == 0.3 为 true

5. float64 类型的 0.1 + 0.2 运算

float64 类型的小数包含:1 位符号位,11 位阶码,52 位尾码,使用 “乘 2 取整法” 分别计算 0.1、0.2、0.3 的二进制指数表示形式:

扫描二维码关注公众号,回复: 10845454 查看本文章
0.1 => 1.1001100110011001100110011001100110011001100110011010 * 2^(-4) (小数位 53 位为 1 ,连续进位)
0.2 => 1.1001100110011001100110011001100110011001100110011010 * 2^(-3) (小数位 53 位为 1 ,连续进位)
0.3 => 1.0011001100110011001100110011001100110011001100110011 * 2^(-2) (小数位 53 位为 0 ,舍去)

计算 0.1 + 0.2,由于阶码不同,需要首先对齐阶码,规则是向大阶码对齐,因此 0.1 的阶码需要由 -4 变为 -3,即:

0.1 => 0.1100110011001100110011001100110011001100110011001101 * 2^(-3)  (原来最右边的一位被舍弃,但此时不会进位)
0.2 => 1.1001100110011001100110011001100110011001100110011010 * 2^(-3)  (対阶前后保持不变)

根据上述表示方法,计算二进制加法 0.1 + 0.2 的结果为:

0.1 + 0.2 = 10.0110011001100110011001100110011001100110011001100111 * 2^(-3) = 1.0011001100110011001100110011001100110011001100110100 * 2^(-2)
0.3 => 1.0011001100110011001100110011001100110011001100110011 * 2^(-2)

由于 0.1 + 0.2 在计算机底层存储与 0.3 的底层存储不一致,因此 0.1 + 0.2 == 0.3 为 false

6. 常数比较运算

最后一种情况,fmt.Println(0.1+0.2 == 0.3) ,这源于 go 编译器对于常数运算已经在编译期就算好了,至于细节,有时间再细聊!

7. 结论

浮点数因为在计算中会丢失精度,因此不能直接比较是否相等,而应该使用一个极小值,判断两个数的差值小于极小值时,即认为二者相等!虽然某些时候,计算机误打误撞恰好判断正确,但我们仍应该使用最稳妥的方式编程,避免引入不必要的 bug,而且要知道这种错误很难定位。

发布了272 篇原创文章 · 获赞 93 · 访问量 39万+

猜你喜欢

转载自blog.csdn.net/shida_csdn/article/details/104602189