也许你知道 0.1 + 0.2 === 0.3 为 false,但是 1.1 + 0.2 === 1.3 呢?

因吹斯挺

在浏览器调试窗口中输入下面两段代码,会发现一个因吹斯挺的现象:

1
2
console.log(0.1 + 0.2 === 0.3) // false
console.log(1.1 + 0.2 === 1.3) // true

明明都是浮点数的加法,为什么表现出来的效果不一样呢?让我们一步步来揭晓谜底。

十进制转二进制

首先我们需要知道十进制是怎么转为二进制的,下面以 6.1 为例来进行说明。

整数部分

整数部分转为二进制如下图所示:

1
2
3
4
5
6 / 2 = 3...0  => 0
3 / 2 = 1...1 => 1
1 / 2 = 0...1 => 1

6 => 110

也就是不断的将商除以二得到余数,直到商为 0。

小数部分

小数部分转为二进制如下图所示:

1
2
3
4
5
6
7
8
9
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。

结合两部分,得到:

1
110.00011001100110011001100110011001100110011001100110011

转化为科学计数法:

1
1.1000011001100110011001100110011001100110011001100110011×2^(2)

浮点数在计算机中如何存储

双精度浮点数在计算机中存储原理如下图所示:

其中,sign 为 0 表示正数,为 1 表示负数,exponent 表示科学计数法中的指数部分,加上一个偏移值 1023,fraction 表示小数点后的部分,整数部分永远为 1,计算机不存储,但是运算的时候会加上。

下面推导下 6.1 的表示方法:

1
2
3
4
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的运算过程(方括号表示实际不存储的整数部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 稍加改造,变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

附录

提供一段 c 语言代码,用来获取 double 型数据在内存中的表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>
int main(int argc, const char * argv[]) {
double data;
unsigned long long int buff;
int i;
char s[66];
data = (double)0.1;
memcpy(&buff, &data, 8);
for(i = 65; i >=0; i--) {
if (i == 1 || i == 13) {
s[i] = '-';
} else {
if(buff%2 == 1){
s[i] = '1';
} else {
s[i] = '0';
}
buff /= 2;
}
}

printf("%s\n", s);
}

参考