如果你以前沒了解過類似的坑,乍一看似乎覺得不可思議。但是某些語言下事實確實如此(比如 Javascript):
再看個例子,+1 后居然等於原數,沒天理啊!
如果你不知道原因,跟着樓主一起來探究下精度丟失的過程吧。
事實上不僅僅是 Javascript,在很多語言中 0.1 + 0.2 都會得到 0.30000000000000004,為此還誕生了一個好玩的網站 0.30000000000000004。究其根本,這些語言中的數字都是以 IEEE 754 雙精度 64 位浮點數 來存儲的,它的表示格式為:
(s) * (m) * (2^e)
s 是符號位,表示正負。m 是尾數,有 52 bits。e 是指數,有 11 bits,e 的范圍是 [-1074, 971](ECMAScript 5 規范),這樣其實很容易推出 Javascript 能表示的最大數為:
1 * (Math.pow(2, 53) - 1) * Math.pow(2, 971) = 1.7976931348623157e+308
而這個數也就是 Number.MAX_VALUE
的值。
同理可推得 Number.MIN_VALUE
的值:
1 * 1 * Math.pow(2, -1074) = 5e-324
需要注意的是,Number.MIN_VALUE
表示的是最小的比零大的數,而不是最小的數,最小的數很顯然是 -Number.MAX_VALUE。
可能你已經注意到,當計算 Number.MAX_VALUE
時,(Math.pow(2, 53) - 1)
的結果用二進制表示是 53 個 1,除了 m 表示的 52 個 bits 外,其實最前面的 1 bit 是隱藏位(隱藏位表示的永遠是 1),設置隱藏位為的是能表示更大范圍的數。(對於隱藏位我也不是很清楚,一說 "當 指數 e 的二進制位全為 0 時,隱藏位為 0,如果不全為 0,則隱藏位為 1,這應該是基於指數表達式的存儲方式決定的,隱藏位也就是指數的底數里面的整數部分,尾數 m 則是指數中底數的 fraction 小數部分" 詳見 Javascript 中小數和大整數的精度丟失問題)
復習了一些組成原理的知識后,我們再回到 0.1 + 0.2 這道題本身。我們都知道,計算機中的數字都是以二進制存儲的,如果要計算 0.1 + 0.2 的結果,計算機會先把 0.1 和 0.2 分別轉化成二進制,然后相加,最后再把相加得到的結果轉為十進制。
我們先把 0.1 和 0.2 分別轉化為二進制,十進制轉為二進制這里就不多說了,整數部分 "除二取余,倒序排列",小數部分 "乘二取整,順序排列"。也可以用 Javascript 的 toString(2)
方法驗證轉換的結果。
// 0.1 轉化為二進制
0.0 0011 0011 0011 0011...(0011循環)
// 0.2 轉化為二進制
0.0011 0011 0011 0011 0011...(0011循環)
當然計算機並不能表示無限小數,畢竟只有有限的資源,於是我們得把它們用 IEEE 754 雙精度 64 位浮點數 來表示:
e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
當然,真實的計算機存儲中 m 並不會是一個小數,而是上面的小數點后的 52 bits,小數點前的 1 為隱藏位。
這里又出現一個問題,雖然我們已經明確 m 只能有 52 位(小數點后),但是如果第 53 位是 1,是該進位還是不進位?這里需要考慮 IEEE 754 Rounding modes,可以看下這篇文章 浮點數解惑,或者聽我簡單地解釋下。
關於默認的舍入規則,簡單的說,如果 1.101 要保留一位小數,可能的值是 1.1 和 1.2,那么先看 1.101 和 1.1 或者 1.2 哪個值更接近,毫無疑問是 1.1,於是答案是 1.1。那么如果要保留兩位小數呢?很顯然要么是 1.10 要么是 1.11,而且又一樣近,這時就要看這兩個數哪個是偶數(末位是偶數),保留偶數為答案。綜上,如果第 52 bit 和 53 bit 都是 1,那么是要進位的。
另外,相加時如果指數不一致,需要對齊,一般情況下是向右移,因為最右邊的即使溢出了,損失的精度遠遠小於左邊溢出。
接下去就不難了:
e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
---------------------------------------------------------------------------
e = -3; m = 0.1100110011001100110011001100110011001100110011001101
+ e = -3; m = 1.1001100110011001100110011001100110011001100110011010
---------------------------------------------------------------------------
e = -3; m = 10.0110011001100110011001100110011001100110011001100111
---------------------------------------------------------------------------
e = -2; m = 1.0011001100110011001100110011001100110011001100110100(52位)
---------------------------------------------------------------------------
= 0.010011001100110011001100110011001100110011001100110100
= 0.30000000000000004(十進制)
而 9007199254740992 + 1 = 9007199254740992
的推理過程大同小異。
9007199254740992 其實就是 2 ^ 53。
e = 0; m = 100000000000000000000000000000000000000000000000000000 (53個0)
+ e = 0; m = 1
---------------------------------------------------------------------------
e = 0; m = 100000000000000000000000000000000000000000000000000001
因為 m 只能有 52 位,而上面相加兩數相加后 m 有 53 位(已經除去首位隱藏位),又因為 Rounding modes 的偶數原則,所以將 53 bit 的 1 舍去,所以大小跟 2 ^ 52 並沒有變化,試想下,如果是 + 2,那么結果就不一樣了。(ps:其實 2^53 在計算機存儲中的 m 只能有 52 位,即只有 52 個 0)
事實上,當結果大於 Math.pow(2, 53) 時,會出現精度丟失,導致最終結果存在偏差,而當結果大於 Number.MAX_VALUE,直接返回 Infinity。
如果你覺得已經足夠了解 IEEE 754 雙精度 64 位浮點數 的運算性質了,不妨試試 玉伯 在 JavaScript 中小數和大整數的精度丟失 一文最后留下的思考題:
Number.MAX_VALUE + 1 == Number.MAX_VALUE;
Number.MAX_VALUE + 2 == Number.MAX_VALUE;
...
Number.MAX_VALUE + x == Number.MAX_VALUE;
Number.MAX_VALUE + x + 1 == Infinity;
...
Number.MAX_VALUE + Number.MAX_VALUE == Infinity;
// 問題:
// 1. x 的值是什么?
// 2. Infinity - Number.MAX_VALUE == x + 1; 是 true 還是 false ?
2016-07-21 補:
之前類似如此的精度缺失問題,我都會推薦先將其乘以 10 的倍數,化為整數的方式:
(0.1 * 10 + 0.2 * 10) / 10
=> 0.3
直到看到此文 你不一定知道的幾個前端小知識:
2177.74*100
=> 217773.99999999997
樓主不禁又陷入了思考...