为什么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

数学中计算时,我们需要将指数位置对齐,但需要指明的是JS中没有采用Exponent Bias,而是将尾数Mantissa视为为整数计算的,这样误差会增大,但是实现算法简单。
1.1001100110011001100110011001100110011001100110011010 (Exponent:-4)+ // 0.1
1.1001100110011001100110011001100110011001100110011010 (Exponent:-3)= // 0.2

这里有一个问题,就是指数不一致时,应该怎么处理,一般是往右移,因为即使右边溢出了,损失的精度远远小于左移时的溢出。
0.11001100110011001100110011001100110011001100110011010 (Exponent:-3)+ // 0.1
1.10011001100110011001100110011001100110011001100110100 (Exponent:-3)= // 0.2
10.01100110011001100110011001100110011001100110011001110 (Exponent:-3)
上式结果经过两步转化,得到最终结果:
1.001100110011001100110011001100110011001100110011001110 (Exponent:-2) // 54 位,要转化为 52位,需要进行舍入
1.0011001100110011001100110011001100110011001100110100 (Exponent:-2)

转换为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)+ // 0.1
1.001100110011001100110011001100110011001100110011001100 (Exponent:-2)= // 0.3
1.100110011001100110011001100110011001100110011001100110 (Exponent:-2) // 54 位,二进制舍入后
1.1001100110011001100110011001100110011001100110011010 (Exponent:-2) // 52 位

最终的结果,恰好等于 0.4。

结论

JS 浮点数转化为二进制数进行存储,由于存储的长度有限制,就会有数据的舍入而导致精度丢失。所以在浮点数的计算,都会有精度丢失,即使结果看起来正确,也只是碰巧而已。

上一篇:BigInteger与BigDecimal


下一篇:剑指Offer第11题(数值的整数次方)