为什么0.1+0.2 !== 0.3,而 0.1+0.3 === 0.4

最近看了一本书《代码之髓》,里面提到浮点数在计算机的存储方式——IEEE 754 会引起浮点数的精度丢失问题。这让我想起了“著名”的 JS 问题:为什么 0.1 + 0.2 !== 0.3 ?

迷迷糊糊的就记得是浮点数精度丢失原因造成的,但问到具体是怎么回事儿就傻眼了。

今天就尝试用基本知识来推理下。

十进制浮点数转二进制

众所周知,所有数据都是以二进制形式保存在计算机中的。浮点数如何转化为二进制数呢?

  • 整数部分:除以2,取出余数,商继续除以2,直到得到0为止,将取出的余数逆序。
  • 小数部分:乘以2,然后取出整数部分,将剩下的小数部分继续乘以2,然后再取整数部分,一直取到小数部分为零为止。如果永远不为零,则按要求保留足够位数的小数,最后一位做0舍1入。将取出的整数顺序排列。

譬如对于 8.75, 转二进制计算过程如下:

8/2:4 余 0,
4/2:2 余 0,
2/2:1 余 0,
1/2:0 余 1
所以 8 的二进制为 1000

0.75*2 = 1.5,取整 1,小数部分为 0.5,
0.5*2 = 1.0,取整 1,小数部分为 0
所以 0.75 的二进制是 0.11

最终得到 8.75 等于二进制数 1000.11。

0.1,0.2,0.3,0.4 的二进制转化

通过上面的计算方式,可以得出 0.1,0.2,0.3,0.4 对应的二进制数:

// 括号内表示数字无限循环
0.1 -> 0.000110011(0011)
0.2 -> 0.00110011(0011)
0.3 -> 0.010011(0011)
0.4 -> 0.0110011(0011)

JS 数字存储方式

在实际存储中,不可能保存无限长度的数据,JS 采用 IEEE 754 双精度64位浮点数来保存数字,格式为s * m * (2^(e)),其中 s 表示符号位,m 表示尾数占52位,e 表示指数占11位。

我们来看看上面几个数在计算机内的表示,也可以在这个网站验证结果:

// 0.1
e = -4;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

// 0.2
e = -3;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

// 0.3
e = -2
m = 1.0011001100110011001100110011001100110011001100110011 (52位)

// 0.4
e = -2
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

特别注意的是,对于无限长度的数据,在存储过程中,会有数据的舍入【二进制向最近偶数舍入】,这造成了后面数据计算的误差。

0.1 + 0.2 !== 0.3

现在我们来做二进制的加法计算:
1.1001100110011001100110011001100110011001100110011010 (Exponent:-4)+ // 0.1
1.1001100110011001100110011001100110011001100110011010 (Exponent:-3)= // 0.2

这里有一个问题,就是指数不一致时,应该怎么处理,一般是往右移,因为即使右边溢出了,损失的精度远远小于左移时的溢出。

也就是指数往更大值看起,尾数不够添就 0。

0.11001100110011001100110011001100110011001100110011010 (Exponent:-3)+ // 指数由 -4 变为 -3,小数部分右移 1 位
1.10011001100110011001100110011001100110011001100110100 (Exponent:-3)= // 尾数长度不够,加一个0
10.01100110011001100110011001100110011001100110011001110 (Exponent:-3)

计算结果小数部分 53 位,整数部分为 10,转换为IEEE754双精度为 1.0011001100110011001100110011001100110011001100110100 * 2^(-2)。

如果用二进制转成十进制为 2^(-2) + 2^(-5) + 2^(-6)...。 结果大约是0.30000000000000004419,去小数点后面17位精度为0.30000000000000004。

0.1 + 0.3 === 0.4

1.1001100110011001100110011001100110011001100110011010 (Exponent:-4)+ // 0.1
1.0011001100110011001100110011001100110011001100110011 (Exponent:-2) // 0.3

这里的处理方式同上:

0.011001100110011001100110011001100110011001100110011010 (Exponent:-2)+ // 指数由- 4变为 -2,小数部分右移 2 位
1.001100110011001100110011001100110011001100110011001100 (Exponent:-2)= // 尾数不够,加两个 0
1.100110011001100110011001100110011001100110011001100110 (Exponent:-2)

计算结果有 54 位,保存时需要二进制舍入,得到:
1.1001100110011001100110011001100110011001100110011010 (Exponent:-2) // 52 位

这个结果,恰好等于 0.4。

结论

JS 浮点数转化为二进制数进行存储,由于存储的长度有限制,就会有数据的舍入而导致精度丢失。

浮点数的计算,会转化为二进制进行计算,计算结果又可能丢失精度。即使结果看起来正确,也只是碰巧而已。

posted @ 2020-11-04 15:25  Liaofy  阅读(858)  评论(0编辑  收藏  举报