1. 程式人生 > 實用技巧 >為什麼0.1+0.2 !== 0.3,而 0.1+0.3 === 0.4

為什麼0.1+0.2 !== 0.3,而 0.1+0.3 === 0.4

最近看了一本書《程式碼之髓》,裡面提到浮點數在計算機的儲存方式——IEEE 754 會引起浮點數的精度丟失問題。這讓我想起了“著名”的 JS 問題:為什麼 0.1 + 0.2 !== 0.3 ?

迷迷糊糊的就記得是浮點數精度丟失原因造成的,但問到具體是怎麼回事兒就傻眼了。

今天就嘗試用基本知識來推理下。

十進位制浮點數轉二進位制

眾所周知,所有資料都是以二進位制形式儲存在計算機中的。浮點數如何轉化為二進位制數呢?

  • 整數部分:除以2,取出餘數,商繼續除以2,直到得到0為止,將取出的餘數逆序。
  • 小數部分:乘以2,然後取出整數部分,將剩下的小數部分繼續乘以2,然後再取整數部分,一直取到小數部分為零為止。如果永遠不為零,則按要求保留足夠位數的小數,最後一位做0舍1入。將取出的整數順序排列。

譬如對於 8.75, 轉二進位制計算過程如下:

8/2:4 餘 0,
4/2:2 餘 0,
2/2:1 餘 0,
1/2:0 餘 1
所以 8 的二進位制為 1000

0.75*2 = 1.5,取整 1,小數部分為 0.5,
0.5*2 = 1.0,取整 1,小數部分為 0
所以 0.75 的二進位制是 0.11

最終得到 8.75 等於二進位制數 1000.11。

0.1,0.2,0.3,0.4 的二進位制轉化

通過上面的計算方式,可以得出 0.1,0.2,0.3,0.4 對應的二進位制數:

// 括號內表示數字無限迴圈
0.1 -> 0.000110011(0011)
0.2 -> 0.00110011(0011)
0.3 -> 0.010011(0011)
0.4 -> 0.0110011(0011)

JS 數字儲存方式

在實際儲存中,不可能儲存無限長度的資料,JS 採用 IEEE 754 雙精度64位浮點數來儲存數字,格式為s * m * (2^e),其中 s 表示符號位,m 表示尾數佔52位,e 表示指數佔11位。

我們來看看上面幾個數在計算機內的表示,也可以在這個網站驗證結果:

// 0.1
e = -4;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

// 0.2
e = -3;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

// 0.3
e = -2
m = 1.0011001100110011001100110011001100110011001100110011 (52位)

// 0.4
e = -2
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

特別注意的是,對於無限長度的資料,在儲存過程中,會有資料的舍入【二進位制向最近偶數舍入】,這造成了後面資料計算的誤差。

0.1 + 0.2 !== 0.3

數學中計算時,我們需要將指數位置對齊,但需要指明的是JS中沒有采用Exponent Bias,而是將尾數Mantissa視為為整數計算的,這樣誤差會增大,但是實現演算法簡單。
1.1001100110011001100110011001100110011001100110011010 (Exponent:-4)+ // 0.1
1.1001100110011001100110011001100110011001100110011010 (Exponent:-3)= // 0.2

這裡有一個問題,就是指數不一致時,應該怎麼處理,一般是往右移,因為即使右邊溢位了,損失的精度遠遠小於左移時的溢位。
0.11001100110011001100110011001100110011001100110011010 (Exponent:-3)+ // 0.1
1.10011001100110011001100110011001100110011001100110100 (Exponent:-3)= // 0.2
10.01100110011001100110011001100110011001100110011001110 (Exponent:-3)
上式結果經過兩步轉化,得到最終結果:
1.001100110011001100110011001100110011001100110011001110 (Exponent:-2) // 54 位,要轉化為 52位,需要進行舍入
1.0011001100110011001100110011001100110011001100110100 (Exponent:-2)

轉換為IEEE754雙精度為 1.0011001100110011001100110011001100110011001100110100 * 2(-2),如果用二進位制轉成十進位制為(2(-2)+2(-5)+2(-6)...)。 結果大約是0.30000000000000004419,去小數點後面17位精度為0.30000000000000004。

0.1 + 0.3 === 0.4

1.1001100110011001100110011001100110011001100110011010 (Exponent:-4)+ // 0.1
1.0011001100110011001100110011001100110011001100110011 (Exponent:-2) // 0.3

這裡有一個問題,就是指數不一致時,應該怎麼處理,一般是往右移,因為即使右邊溢位了,損失的精度遠遠小於左移時的溢位。

0.011001100110011001100110011001100110011001100110011010 (Exponent:-2)+ // 0.1
1.001100110011001100110011001100110011001100110011001100 (Exponent:-2)= // 0.3
1.100110011001100110011001100110011001100110011001100110 (Exponent:-2) // 54 位,二進位制舍入後
1.1001100110011001100110011001100110011001100110011010 (Exponent:-2) // 52 位

最終的結果,恰好等於 0.4。

結論

JS 浮點數轉化為二進位制數進行儲存,由於儲存的長度有限制,就會有資料的舍入而導致精度丟失。所以在浮點數的計算,都會有精度丟失,即使結果看起來正確,也只是碰巧而已。