探索 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

相關文章
相關標籤/搜索