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

4,225 阅读9分钟

因吹斯挺

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

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)

附录

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

#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);
}

参考