为什么0.1 + 0.2 !== 0.3

602 阅读5分钟

为什么 0.1 + 0.2 !== 0.3 ?这是一道高频面试题,考查的就是应聘者基本功是否扎实,今天就来分析一下为什么计算机认为它们是不相等的。

浮点数的存储方式

首先要知道浮点数在计算机中是如何存储的。JavaScript 用 8 个字节,也就是 64 个比特位来表示一个数字,这 64 个比特位,每个位置上要么是 0 要么是 1。

其次要知道计算机跟人脑最大的不同就是计算机只认识 2 进制,而人脑用的是 10 进制,所以需要先把 0.1 和 0.2 转换成二进制来分析:

0.1.toString(2) // 0.00011001100110011001100110011001100110011001100110011... 无限循环
0.2.toString(2) // 0.0011001100110011001100110011001100110011001100110011... 无限循环

正所谓 “吾生也有涯,而知也无涯。以有涯随无涯,殆已” 意思是说用有限的 64 个比特位来表示无限循环的小数那是不可能的,只能近似表示。

那计算机是如何用 2 进制来近似表示 0.1 的呢?按照 IEEE 754 标准,把这 64 个比特位划分为 64 = 1 + 11 + 52 三个部分:

x xxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1 ----11----- -------------------------52-------------------------
  • 第 1 位是符号位 S(sign),用来表示正负,0表示正数,1表示负数
  • 第 2 位到第 12 位是指数位 E(exponent),用来储存指数部分
  • 第 13 位到第 64 位是尾数位 M(mantissa),用来储存有效数字

为什么要分成这三个部分呢?我们不妨先思考一下,在十进制的情况下如何统一表示整数和小数。

12345 = 1 * 10^4 * 1.2345
-0.038 = -1 * 10^(-2) * 3.8 

有没有发现,无论是正数还是负数还是小数,都可以用这种方式来表示,即科学计数法,只要知道了符号、指数和有效数字,那么这个数就被确定下来了。在二进制当中也是类似的,例如:

100110 = 1 * 2^5 * 1.0011
-0.0001100110011 = -1 * 2^(-4) * 1.100110011

不过二进制中有一个特殊的地方就是有效数字部分的整数位永远都是1,所以在存储的时候可以只存储小数尾数部分,以 -0.0001100110011 为例:

  • 符号位存 1 表示负数
  • 指数为存 -4
  • 尾数部分存小数尾数部分,即 100110011

关键是指数位置的 -4 咋存呢?我们知道 11 个比特能表示 2^11 = 2048 个不同的数字,即 0 ~ 2047,如果考虑负数的话,范围是 -1023 ~ 1024,那么存储 -4 可以有两种方案:

  • 把 11 位指数部分的首位设置成符号位,用后 10 位存储值
  • 统一加上 1023 之后再存储,这样的话肯定是无符号的整数了

IEEE 754 标准采用的就是后者,所以指数部分存的是 1023 - 4 = 1019 的二进制 1111111011。

安全数和最大数

JS 中有个最大安全整数:

Number.MAX_SAFE_INTEGER = 9007199254740991 // 2**53 - 1 
// 表示方式为:1 * 2^52 * 1.1111111111111111111111111111111111111111111111111111
// 就是尾数部分的 52 个比特位已经全是 1 了
parseInt('11111111111111111111111111111111111111111111111111111', 2) // 9007199254740991

大于这个值的数就被认为是不安全的,因为如果再多 1 的话,至少要用 53 个比特位来表示,即:

1 * 2^53 * 1.00000000000000000000000000000000000000000000000000000 // 2**53
1 * 2^53 * 1.00000000000000000000000000000000000000000000000000001 // 2**53+1

由于尾数部分只能存储 52 个比特,所以第 53 个比特位就丢掉了,也就是说 2**532**52+1 的表示方式是一样的,它们的结果也是一样的:

2**53 = 9007199254740992
2**53+1 = 9007199254740992
2**53 === 2**53+1 // true

所以说:

  • 2^53 ~ 2^54 之间的数只能精确表示偶数,因为最后一个比特位被丢掉了
  • 2^54 ~ 2^55 之间的数只能精确表示4的倍数,因为最后两个比特位被丢掉了
  • 2^55 ~ 2^56 之间的数只能精确表示8的倍数,因为最后三个比特位被丢掉了
  • 以此类推,丢掉 N 个比特位就只能精确表示 2^N 的倍数

由于指数位最大值为 1024,所以能表示的最大数就是:

1 * 2^1024 * 1.00000000000000000000000000000000000000000000000000000000000...

此时指数部分的比特位满了,位数部分的比特位也满了,任何超过这个值的数都将等于这个数,所以 JS 把这个数定义为 Infinity:

2**1024 // Infinity

JS 能表示的最大数就是:

1 * 2^1023 * 1.1111111111111111111111111111111111111111111111111111
Number.MAX_VALUE // 1.7976931348623157e+308

计算 0.1 + 0.2

先看 0.1 和 0.2 的存储方式:

0.1.toString(2) 
// 实际值:0.00011001100110011001100110011001100110011001100110011001100... 无限循环
// 科学计数法:1 * 2^(-4) * 1.1001100110011001100110011001100110011001100110011001  保留 52 为
// 也就是说存储值:0.0001100110011001100110011001100110011001100110011001101


0.2.toString(2) 
// 实际值:0.0011001100110011001100110011001100110011001100110011... 无限循环
// 科学计数法:1 * 2^(-3) * 1.1001100110011001100110011001100110011001100110011001 保留 52 为
// 也就是说存储值:0.001100110011001100110011001100110011001100110011001101

计算它们的和:

0.0001100110011001100110011001100110011001100110011001101 
0.001100110011001100110011001100110011001100110011001101
-----------------------------------------------------------
0.0100110011001100110011001100110011001100110011001100111

科学计数法表示为:1 * 2^(-2) * 1.0011001100110011001100110011001100110011001100110011

而这个结果转成十进制正好是 0.30000000000000004