因吹斯挺
在浏览器调试窗口中输入下面两段代码,会发现一个因吹斯挺的现象:
console.log(0.1+0.2===0.3) // false
console.log(1.1+0.2===1.3) // true
复制代码
明明都是浮点数的加法,为什么表现出来的效果不一样呢?让我们一步步来揭晓谜底。
十进制转二进制
首先我们需要知道十进制是怎么转为二进制的,下面以 6.1 为例来进行说明。
整数部分
整数部分转为二进制如下图所示:
6 / 2 = 3...0 => 0
3 / 2 = 1...1 => 1
1 / 2 = 0...1 => 1
6 => 110
复制代码
也就是不断的将商除以二得到余数,直到商为0。
小数部分
小数部分转为二进制如下图所示:
0.1 * 2 = 0.2 => 0
0.2 * 2 = 0.4 => 0
0.4 * 2 = 0.8 => 0
0.8 * 2 = 1.6 => 1
0.6 * 2 = 1.2 => 1
0.2 * 2 = 0.4 => 0
…
0.1 => 000110011001100110011001100110011001100110011001100110011...
复制代码
不断的乘以二然后拿掉整数部分,直到积为0。
结合两部分,得到:
110.00011001100110011001100110011001100110011001100110011
复制代码
转化为科学计数法:
1.1000011001100110011001100110011001100110011001100110011×2^(2)
复制代码
浮点数在计算机中如何存储
双精度浮点数在计算机中存储原理如下图所示:
其中,sign
为 0 表示正数,为 1 表示负数,exponent
表示科学计数法中的指数部分,加上一个偏移值 1023,fraction
表示小数点后的部分,整数部分永远为 1,计算机不存储,但是运算的时候会加上。
下面推导下 6.1 的表示方法:
sign: 0
exponent: 2 + 1023 => 10000000001
fraction: 1000011001 1001100110 0110011001 1001100110 0110011001 10 011 (只能保留52位,多余部分向偶舍入)
=> 1000011001 1001100110 0110011001 1001100110 0110011001 10
复制代码
其中,向偶舍入可参考浮点数向偶数舍入的问题
浮点数加法
知道了浮点数的表示方法,下面我们来看看0.1+0.2
的运算过程(方括号表示实际不存储的整数部分):
0.1 => 0 01111111011[1]1001100110011001100110011001100110011001100110011010
+
0.2 => 0 01111111100[1]1001100110011001100110011001100110011001100110011010
1. 对齐指数,小的往大的对齐。所以 0.1 指数部分加一,小数点需要往左移一位,超出部分向偶舍入
0.1 => 0 01111111100[0]1100110011001100110011001100110011001100110011001101 0
0.1 => 0 01111111100[0]1100110011001100110011001100110011001100110011001101
2. 小数部分相加
0.1 => 0 01111111100[0]1100110011001100110011001100110011001100110011001101
+
0.2 => 0 01111111100[1]1001100110011001100110011001100110011001100110011010
Res => [10]0110011001100110011001100110011001100110011001100111
3. 小数部分相加的结果超出了52位,小数点要左移一位,多余部分要向偶舍入
Res => 0 01111111101[1]0011001100110011001100110011001100110011001100110011 1
Res => 0 01111111101[1]0011001100110011001100110011001100110011001100110100
4. 推导 0.3 的表示
0.3 => 0 01111111101[1]0011001100110011001100110011001100110011001100110011
复制代码
显然,小数部分后四位是不相等的,并且通过对比我们可以知道 0.1+0.2 其实是大于 0.3 的。
下面继续推导 1.1+0.2
的运算过程:
1.1 => 0 01111111111[1]0001100110011001100110011001100110011001100110011010
+
0.2 => 0 01111111100[1]1001100110011001100110011001100110011001100110011010
1. 对齐指数,小的往大的对齐。所以 0.2 指数部分加三,小数点需要往左移三位,超出部分向偶舍入
0.2 => 0 01111111111[0]0011001100110011001100110011001100110011001100110011 010
0.2 => 0 01111111111[0]0011001100110011001100110011001100110011001100110011
2. 小数部分相加
1.1 => 0 01111111111[1]0001100110011001100110011001100110011001100110011010
+
0.2 => 0 01111111111[0]0011001100110011001100110011001100110011001100110011
Res => 0 01111111111[1]0100110011001100110011001100110011001100110011001101
3. 推导 1.3 的表示
1.3 => 0 01111111111[1]0100110011001100110011001100110011001100110011001101
复制代码
经过对比发现,两者确实是相等的。
问题
可以再提供一个例子吗?
通过观察我们发现,造成不相等的原因是因为小数部分超过52位长度的时候有向偶进位的过程,所以我们只要绕过这个过程就好了。比如,我们对 0.1+0.2
稍加改造,变成这样:
0 01111111011[1]0000000000000000000000000000000000000000000000000000
+
0 01111111100[1]0000000000000000000000000000000000000000000000000000
=>
0 01111111100[0]1000000000000000000000000000000000000000000000000000 0
+
0 01111111100[1]0000000000000000000000000000000000000000000000000000
=>
0 01111111100[0]1000000000000000000000000000000000000000000000000000
+
0 01111111100[1]0000000000000000000000000000000000000000000000000000
=
0 01111111100[1]1000000000000000000000000000000000000000000000000000
复制代码
即 0.0625+0.125
。
更一般的,我们有 2^(-m) + 2^(-n)
。