閱讀本文大約需要8分鍾...
問題
在計算機的世界里,可能有很多常人無法理解的事情。比如 0.1 + 0.2 = ?。來,告訴我你的答案。
有的朋友看到這就迫不及待的說,這么簡單的問題,很明顯等於 0.3 啊,小學生都會算的好伐。你這是在侮辱我的智商?
好吧,我來告訴你一個打臉的事實,0.1 + 0.2 還真不等於 0.3 。先別急着反駁我。
打開你的任意一個瀏覽器(我用chrome做演示),F12打開console控制台,輸入 console.log(0.1 + 0.2) 。如果你操作正確的話,你會看到以下的結果。
是不是感覺匪夷所思,what?為什么結果是0.30000000000000004,這是神魔鬼? 難道,我這么多年學習的數學知識,老師教的都是錯的?
別着急。其實,你的老師教的沒錯,在我們的世界中,0.1 + 0.2 確實是等於 0.3 的。但是,在計算機中,可就不是這么一回事了。待我娓娓道來。
因為,我們在計算數學問題的時候,用的是十進制,計算出來結果是0.3沒問題。但是,在計算機中用的是二進制,都是由0和1來組成。這就不得不提一下,十進制轉換二進制了。
二進制轉換
十進制小數轉換二進制的步驟:(以10.25為例)
1.先轉換整數部分,除2直到商為0,倒數取余。
10/2 ... 商5...余數0
5/2 ...商2...余數1
2/2 ...商1...余數0
1/2 ...商0...余數1
倒數取余,就是1010
2.再轉換小數部分,乘2取整,直到小數部分為0.
0.25*2 ... 0.50 ...整數0
0.50*2 ... 1.0 ...整數1
小數部分為0,結束,即為01
因此10.25(10)轉換成二進制,結果就是 1010.01(2)
聰明的你,類比以上方法,應該可以動手去算一下十進制0.1轉成二進制是多少了。
0.1*2 ... 0.2 ...整數0
0.2*2 ... 0.4 ...整數0
0.4*2 ... 0.8 ...整數0
0.8*2 ... 1.6 ...整數1
0.6*2 ... 1.2 ...整數1
0.2*2 ... 0.4 ...整數0
等等,怎么感覺進入死循環了,小數部分乘以2,一直乘不到小數部分為0
就像十進制中1/3,結果是0.3(3...)這樣的問題一樣,0.1轉成二進制時也會存在精度問題,我們需要進行取舍。
我們看一下0.1在計算機中是怎么存儲的。對此,需要了解一下浮點數的概念。
浮點數
浮點數,顧名思義,小數點是浮動的數。千萬不要以為浮點數就是小數。因為,在js中是沒有整數和小數的概念的,其實整數也是以浮點數的形式表示的,只是小數部分為0而已。
浮點數簡單理解,就是類似於我們十進制中的科學計數法。在計算機中一般遵循IEEE 754標准。格式如下:
(-1)^S * M * 2^E
1. S表示符號位,當S=0時,為正數;當S=1時,為負數。
2. M表示有效數字(尾數),大於等於1,小於2。
3. E為指數(也叫階碼)。
因此,上邊的10.25(二進制1010.01)按照此格式表示即為 1.01001 * 2^3
對於32位浮點數來說,符號位占一位,指數位占8位,尾數占23位
對於64位浮點數來說,符號位占一位,指數位占11位,尾數占52位
IEEE 754標准
注意:IEEE 754標准規定,在保存尾數M時,第一位默認是1,因此可以被舍去,只存儲后邊的部分。例如,1.01001保存的時候,只保存01001,等到用的時候再把1加上去。這樣,就可以節省一個位的有效數字。
指數E在存儲的時候也有些特殊。若為32位,指數占8位,則可表示的大小范圍為0-255 。如為64位,指數占11位,范圍為0-2047 。但是,指數是有正有負的,因此實際值需要在此基礎上減去一個中間數。對於32位,中間數為127,對於64位,中間數為1023 。
還是以1.01001 * 2^3 為例,若為32位浮點數,則需要保存成 3+ 127 = 130,即二進制的10000010,若為64位浮點數,則保存成 3+ 1023 = 1026 ,即二進制的10000000010。
計算步驟
好了。巴拉巴拉了這么多。終於,要進入我們今天的正題了。
我們看一下 0.1 在計算機中是怎么用 IEEE 754標准存儲的。
十進制0.1轉為二進制為0.0001100110011(0011循環),即 1.100110011(0011)*2^-4,因此符號位為0,尾數1.100110011(0011),階碼為 -4,實際存儲為 -4+1023 = 1019 的二進制 1111111011
0 01111111011 1001100110011001100110011001100110011001100110011010
S E指數 M尾數
十進制0.2轉為二進制為0.001100110011(0011循環),即 1.100110011(0011)*2^-3 ,存儲時,符號位為0,尾數 1.100110011(0011),階碼為-3,實際存儲為 -3+1023 = 1020 的二進制 1111111100。
0 01111111100 1001100110011001100110011001100110011001100110011010
S E指數 M尾數
接下來,計算 0.1 + 0.2 。
浮點數進行計算時,需要對階。即把兩個數的階碼設置為一樣的值,然后再計算尾數部分。其實對階很好理解,就和我們十進制科學記數法加法一個道理,先把指數部分化成一樣,再計算尾數。
另外,需要注意一下,對階時需要小階對大階。因為,這樣相當於,小階指數乘以倍數,尾數部分相對應的除以倍數,在二進制中即右移倍數位。這樣,不會影響到尾數的高位,只會移出低位,損失相對較少的精度。
因此,0.1的階碼為 -4 , 需要對階為 0.2的階碼 -3 。尾數部分整體右移一位。
原來的0.1
0 01111111011 1001100110011001100110011001100110011001100110011010
對階后的0.1
0 01111111100 1100110011001100110011001100110011001100110011001101
然后進行尾數部分相加
0 01111111100 1100110011001100110011001100110011001100110011001101
+ 0 01111111100 1001100110011001100110011001100110011001100110011010
= 0 01111111100 10110011001100110011001100110011001100110011001100111
可以看到,產生了進位。因此,階碼需要 +1,即為 -2,尾數部分進行低位四舍五入處理。因尾數最低位為1,需要進位。所以存儲為:
0 1111111101 1011001100110011001100110011001100110011001100110100
最后把二進制轉換為十進制,
二進制為:
1.1011001100110011001100110011001100110011001100110100 * 2^-2
轉為十進制為:
2^-2 * (1*2^0 + 1*2^-1 + 0 + 1*2^-3 + 1*2^-4 + ...)
最終結果為:
0.3000000000000000444089209850062616169452667236328125
因為精度問題,只取到:
0.30000000000000004
問題總結
1.在十進制轉換為二進制的過程中,會產生精度的損失。
2.二進制浮點數進行對階運算時,也會產生精度的損失。
因此,最終結果才產生了偏差。
看完的小伙伴,現在應該能理解,為什么0.1 + 0.2 ≠ 0.3 這個問題了吧。