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
- 符號位元
正數,所以 Sign bit: 0 - 十進位小數轉二進位小數
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) - 組合在一起
Sign + Exponent + Mantissa
0 0111 1110 01 0000 0000 0000 0000 0000 0
例二:0.1
- 符號位元
正數,所以 Sign bit: 0 - 十進位小數轉二進位小數
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 - 組合在一起
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 | console.log(0.1 + 0.2); |
有誤差的兩個數字,去計算出來的結果,「可能」與我們心中用十進位計算出來的結果不一樣。為什麼呢?
有誤差的浮點數計算,會不會與預期結果不一樣?
答案:可能會,也可能不會。簡單來說就是,計算出來的結果,比較靠近哪個數字,就會輸出該數字。
0.1 + 0.3 的結果不是 0.4,但是,該結果最接近 0.4,因此輸出 0.4。
1 | console.log(0.1 + 0.3); // 0.4 與預期結果一樣 |
所以,永遠不要直接比較兩個浮點數的大小。例:if(0.1 + 0.2 + 0.001 === 0.301)
。
沒有誤差的浮點數
要記得,二進位制能精確地表示「位數有限且分母是 2 的倍數的小數」,也就是 0.5、0.625 這種。
浮點數精確計算
要進行浮點數精確地計算,像是金融上的計算時 (你應該不會希望自己銀行裡的錢被四捨五入吧?!),可以運用到像是:
- toFixed 函數:指定浮點數精度
- BigDecimal 概念套件:js-big-decimal、bignumber.js