探索 0.1 + 0.2 的背后

  1. 为何 0.1 + 0.2 !== 0.3?
  2. 为何 Number.MAX_SAFE_INTEGER 值为 Math.pow(2, 53) - 1?
  3. 为何 Math.pow(2, 53) === Math.pow(2, 53) + 1 ?
  4. 为何 Number.MAX_VALUE = 1.7976931348623157e+308 ?
  5. 为何 1.005.toFixed(2) = 1.00 ?

1. 浮点数二进制的存储结构

下图是单精度格式 git

对于单精度浮点数,采用32位存储,最高的1位是符号位s,接着的8位是指数E(-126 ~ 127),剩下的23位为有效数字尾数M。
对于双精度浮点数,采用64位存储,最高的1位是符号位S,接着的11位是指数E(-1022 ~ 1023),剩下的52位为有效数字尾数M。

为何E(双精度)范围是:-1022 ~ 1023 ?github

// E 的范围
E = 0 二进制表示: 0 1111 1111 11 => 2^10 -1 = 1023
E 最大二进制表示(不能全为1):1 1111 1111 10 => 2^11 - 2 = 2046
E 最小二进制表示(不能全为0): 0 0000 0000 01 => 2^0 = 1
则E的范围(1-1023)<= E (2046 - 1023)
复制代码

2. 任何数均可以表示为二进制:1.xxx * 2^e(这是一个规格化的表示),

十进制浮点数转二进制, eg:
bash

0.125
    x       2        ----
-----------------       |
        0.25    0       |
    x      2            |
-----------------       |
        0.5     0       |
    x     2             |
-----------------       |
        1.0     1       ↓

复制代码

13.125, 整数部分13 => 1101, 小数部分 0.125 => 0.001函数

13.125 =>  1101.001 => 1.101001 * 2^3(存储时,小数点前的1省略了,节省空间)
// 以上S=0; E=3(二进制表示为:10000000010); M=101001,
则二进制表示为:
0 10000000010 1010010000000000000000...
- ----------- -------------------------
s E(1023+3)   M,52位长度
复制代码

0.1 转化位二进制: 0.0001100110011(0011循环) => 1.100110011(0011)*2^-4
0.2 转化位二进制: 0.001100110011(0011循环) => 1.100110011(0011)*2^-3spa

3. 运算

  1. 对阶: 小阶码与大阶码对齐
    0.1:1.100110011(0011) * 2^-4 = 0.1100110011(0011) * 2^-3
    0.2:1.100110011(0011)*2^-3
  2. 尾数就近舍入:

a. 多余位 <= 011...11, 舍去。
b. 多余位 = 100...00, 判断尾数的最低有效位的值,若为0则直接舍去,若为1则再加1。
c. 多余位 >= 100...01, 进1。code

0.1 => 0.1100110011001100110011001100110011001100110011001100[1100110011...] => 进1 => 0.1100110011001100110011001100110011001100110011001101
0.2 => 1.1001100110011001100110011001100110011001100110011001[100110011...] => 进1 => 0.1100110011001100110011001100110011001100110011001110

    0.1100110011001100110011001100110011001100110011001101 * 2^-3
+   1.1100110011001100110011001100110011001100110011001110 * 2^-3
----------------------------------------------------------
   10.0110011001100110011001100110011001100110011001100111 * 2^-3
   
=> 1.00110011001100110011001100110011001100110011001100111 * 2^-2
=> 1.0011001100110011001100110011001100110011001100110100 * 2^-2(因为尾数只能52位,就近舍入)

转化为10进制
0.3000000000000000444089209850062616169452667236328125
=> 0.30000000000000004
复制代码

4. 解惑

  1. 为何 0.1 + 0.2 !== 0.3?
    a. 搞清楚number的存储结构;b. 知道十进制二进制之间的转换;c. 搞清楚 运算过程。
  2. 为何 Number.MAX_SAFE_INTEGER 值为 Math.pow(2, 53) - 1?
    由于二进制表示:1.xxx * 2^e , 尾数xxx最多52位,则e<=52时是准确的,超出则会有精度损失,则二进制53个1等于Math.pow(2, 53) - 1。
  3. 为何 Math.pow(2, 53) === Math.pow(2, 53) + 1 ?
    由于1(十进制) => 1 * 2^0(二进制) => 0.000...1 * 2^53(尾数共52个0,一个1,尾数就近舍入) === 0
  • (2^53, 2^54) 之间的数会两个选一个,只能精确表示偶数
  • (2^54, 2^55) 之间的数会四个选一个,只能精确表示4个倍数
  • ... 依次跳过更多2的倍数
Math.pow(2, 53) + 3 === Math.pow(2, 53) + 5 // true
Math.pow(2, 54) === Math.pow(2, 54) + 1 // true
Math.pow(2, 54) === Math.pow(2, 54) + 2 // true
复制代码
  1. 为何 Number.MAX_VALUE = 1.7976931348623157e+308 ? 最大整数为 Math.pow(2, 1024) - 1, 可是2^1024 为 Infinity。
Math.pow(2, 1024) // Infinity
Math.pow(2, 1023) * 1.999999999999999 // 1.797693134862315e+308
复制代码
  1. 为何 1.005.toFixed(2) = 1.00 ? 看山不是山,1.005 实际对应的数字是 1.00499999999999989,在四舍五入时所有被舍去!toPrecision函数可查看精度。

参考文档

coolcao.com/2016/10/12/…
github.com/camsong/blo…cdn

相关文章
相关标签/搜索