【0.1 + 0.2 = 0.30000000000000004】該怎樣理解?
如果你以前沒了解過類似的坑,乍一看似乎覺得不可思議。但是某些語言下事實確實如此(比如 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]
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 分別轉化為二進位制,十進位制轉為二進位制這裡就不多說了,整數部分 "除二取餘,倒序排列",小數部分 "乘二取整,順序排列"。也可以用 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。
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
樓主不禁又陷入了思考...