事實上,不僅僅是 JS,在其他采用 IEEE754 浮點數標准的語言中,0.1 + 0.2 都不會等於 0.3,但是 0.2 + 0.3 卻等於 0.5,這是為何?想必這類問題也困擾着不少程序員。
IEEE754 浮點數的演算
我們知道,科學計數法中 30000 可以寫成 3x104,以 10 為底數 4 為指數的科學計數法。在 IEEE754 標准中是比較類似的,只不過它是二進制數,底數也為 2。
IEEE 754 中最常用的浮點數值表示法是:單精確度(32位)和雙精確度(64位),JavaScript 采用的是后者。舉個例子,十進制數 150,使用雙精度浮點數表示法,表示如下:
// D 表示十進制,B 表示二進制150D = 2^8 * 0.1001011B // 后面省略了 46 個 0
可以通過短除法計算:
150 余數位÷ 2---------------75 0÷ 2---------------37 1÷ 2---------------18 1÷ 2---------------9 0÷ 2---------------4 1÷ 2---------------2 0÷ 2---------------1 0÷ 2---------------0 1
最后一個余數為高位值,於是拿到 150 對應的二進制數位 1001011,也就等於 2^8 * 0.1001011。
上面是整數的表示法,而小數的表示法采用的是乘二取整,如 0.1,它的二進制表示為:
// (0011) 表示循環0.1D = 2^-3 * 0.110011(0011)
其演算方法如下:
0.1 整數位× 2---------------0.2 0× 2---------------0.4 0 * ↓× 2---------------0.8 0× 2---------------1.6 1× 2---------------1.2 1× 2---------------0.4 0 * ↑(0011循環)
與整數不同的是,第一個計算得到的整數位為最高位,故 0.1 對應的二進制數為 0.000110011(0011),也就等於 2^-3 0.1100110011(0011)。
如果一個數既包含整數部分,又包含小數部分,其表示法的計算,需要分拆為整數和小數兩部分,然后相加得到結果。
IEEE754 浮點數精度丟失
IEEE754 浮點數表示法的數據格式如下圖:
// 下圖采用大端表示,高位在左,低位在右。sign exponent fraction+---+----------+---------------------+| 1 | 2~12 | 13~64 |+---+----------+---------------------+
符號位:高位第 1 位,如圖 sign 部分
指數位:高位第 2~12 位,如圖 exponent 部分
尾數位:剩下的 fraction 部分
從上面小數的乘二取整演算中可以看到,有些小數對應的二進制數是無法寫全的,比如 0.1,而 fraction 尾數部分有要求,只允許 52 位,超過部分進一舍零。
那么,我們就可以得到:
0.1D= 2^-4 * 1.10011(0011)B= 2^-4 * 1.10011(0011 repeat 12 times)0011B // ← 最后一位為 1,進 1= 2^-4 * 1.10011(0011 repeat 12 times)010B
揭秘 0.1 + 0.2
根據上面我們了解到的知識,我們可以很容易算出這些值:
0.1D = 2^-4 * 1.1001100110011001100110011001100110011001100110011010B0.2D = 2^-3 * 1.1001100110011001100110011001100110011001100110011010B0.3D = 2^-2 * 1.0011001100110011001100110011001100110011001100110011B
0.1 + 0.2 時,先將兩者指數統一為 -3,故 0.1 小數點向左移一位,於是:
0.1100110011001100110011001100110011001100110011001101B+ 1.1001100110011001100110011001100110011001100110011010B------------------------------------------------------------= 10.0110011001100110011001100110011001100110011001100111B
得到的二進制數為:
10.0110011001100110011001100110011001100110011001100111B
小數點往左移一位使得整數部分為 1,此時尾數部分為 53 位,進一舍零,於是得到最后的值是:
2^-2 * 1.0011001100110011001100110011001100110011001100110100
這個值轉化成真值,結果為:0.30000000000000004。那么 0.1 + 0.2 = 0.30000000000000004 的推演到這里就結束了。
相關驗證
畢竟咱們手動計算可能存在筆誤,可以通過一個叫做 double-bits 的 npm 進行推演,我寫了一個小 demo,感興趣的可以玩耍下:
const db = require('double-bits');const pad = require('pad');// [lo, hi] where lo is a 32 bit integer and hi is a 20 bit integer.const base2Str = (n) => {const f = db.fraction(n);const s = db.sign(n) ? '-' : '';const e = `2^${db.exponent(n) + 1}`;const t = `0.${pad(f[1].toString(2), 20, '0')}${pad(f[0].toString(2), 32, '0')}`;return `${s}${e} * ${t}`;};console.log(base2Str(0.1).toString(2));console.log(base2Str(0.2).toString(2));console.log(base2Str(0.3).toString(2));console.log(base2Str(1.2).toString(2));
上面輸出結果為:
2^-3 * 0.110011001100110011001100110011001100110011001100110102^-2 * 0.110011001100110011001100110011001100110011001100110102^-1 * 0.100110011001100110011110011001100110011001100110011002^1 * 0.10011001100110011001111001100110011001100110011001100
最后
為了按照計算機的思維,IEEE754 的標准來計算 0.1 + 0.2,又重新復習了一遍大學計算機基礎的知識,原碼、反碼、補碼,以及除二取余、乘二取整計算法,最后能夠推演出來,也算是一個勝利吧~