JavaScript / 程式語言中,0.1 + 0.2 != 0.3

電腦科學中小數點如果採用 IEEE754 二進制浮點運算都有同樣的狀況,會出現精度丟失的問題。

十進位浮點數 → IEEE 754 單精度 (32 bits)

先了解一下電腦中儲存小數點的方式,主要是依照 IEEE754。

IEEE 754 單精度Sign(1 bit)、Exponent(8 bits)、Mantissa(23 bits) 三部分組合而成。

例一:0.625

  1. 符號位元
    正數,所以 Sign bit: 0
  2. 十進位小數轉二進位小數
    0.625 * 2 = 1.250
    0.250 * 2 = 0.500
    0.500 * 2 = 1.000
    → 0.101${2}$
    因此,$0.625
    {10}$ = $0.101_{2}$ = $1.01_{2}*2^{-1}$
    Exponent bits: $2^{8-1}+(-1) = 127+(-1) = 126_{10} = 1111110_{2}$ = 0111 1110 (前面多加一個 0 是為了補齊 8 bits)
    Mantissa bits: 01 0000 0000 0000 0000 0000 0 (01 後面的 0 是為了補齊 23 bits)
  3. 組合在一起
    Sign + Exponent + Mantissa
    0 0111 1110 01 0000 0000 0000 0000 0000 0

例二:0.1

  1. 符號位元
    正數,所以 Sign bit: 0
  2. 十進位小數轉二進位小數
    0.1 * 2 = 0.2
    0.2 * 2 = 0.4
    0.4 * 2 = 0.8
    0.8 * 2 = 1.6
    0.6 * 2 = 1.2
    0.2 * 2 = 0.4
    → 0.000110…${2}$
    因此,$0.1
    {10}$ = $0.00011\overline{0011}{2}$ = $1.1\overline{0011}{2}*2^{-4}$
    Exponent bits: $2^{8-1}+(-4) = 127+(-4) = 123_{10} = 1111011_{2}$ = 0111 1011 (前面多加一個 0 是為了補齊 8 bits)
    Mantissa bits: 1 0011 0011 … (無限循環)
    0 捨 1 入到 23 位是:1 0011 0011 0011 0011 0011 001 + 1 = 1 0011 0011 0011 0011 0011 01
  3. 組合在一起
    Sign + Exponent + Mantissa
    0 0111 1011 1 0011 0011 0011 0011 0011 01

但是 0 0111 1011 1 0011 0011 0011 0011 0011 01 轉換回二進位小數是:
Mantissa bits: 1 0011 0011 0011 0011 0011 01
→ 1.1 0011 0011 0011 0011 0011 01
→ 1.1 0011 0011 0011 0011 0011 01 * $2^{-4}$
→ 0.00011 0011 0011 0011 0011 0011 01
= $2^{-4}+2^{-5}+2^{-8}+2^{-9}+2^{-12}+2^{-13}+2^{-16}+2^{-17}+2^{-20}+2^{-21}+2^{-24}+2^{-25}+2^{-27}$
= 0.100000001490116119384765625 (這是真實儲存的數值)

例三:0.2

$0.2_{10}$ = $0.0011\overline{0011}{2}$ = $1.1\overline{0011}{2}*2^{-3}$
Exponent bits: $2^{8-1}+(-3) = 127+(-3) = 124_{10} = 1111100_{2}$ = 0111 1100
用 IEEE 754 32 bits 表示為:
0 0111 1100 1 0011 0011 0011 0011 0011 01

真實儲存數值:0.20000000298023223876953125

0.1 + 0.2 !== 0.3

實際上儲存時,0.1 和 0.2 都不是 0.1 和 0.2,相加起來當然不等於 0.3。

1
2
console.log(0.1 + 0.2);
// 0.30000000000000004

有誤差的兩個數字,去計算出來的結果,「可能」與我們心中用十進位計算出來的結果不一樣。為什麼呢?

有誤差的浮點數計算,會不會與預期結果不一樣?

答案:可能會,也可能不會。簡單來說就是,計算出來的結果,比較靠近哪個數字,就會輸出該數字。

0.1 + 0.3 的結果不是 0.4,但是,該結果最接近 0.4,因此輸出 0.4。

1
2
3
4
console.log(0.1 + 0.3); // 0.4 與預期結果一樣
console.log(0.1 + 0.2 + 0.001);
// 0.30100000000000005
// 與預期結果不一樣,它也不是 0.30100000000000004

所以,永遠不要直接比較兩個浮點數的大小。例:if(0.1 + 0.2 + 0.001 === 0.301)

沒有誤差的浮點數

要記得,二進位制能精確地表示「位數有限且分母是 2 的倍數的小數」,也就是 0.5、0.625 這種。

浮點數精確計算

要進行浮點數精確地計算,像是金融上的計算時 (你應該不會希望自己銀行裡的錢被四捨五入吧?!),可以運用到像是:

  1. toFixed 函數:指定浮點數精度
  2. BigDecimal 概念套件:js-big-decimalbignumber.js