為什麼0.1+0.2不等於0.3?
在python的控制台也是輸出這個數:
在C裡面運行以下代碼,指定輸出小數位為57位:
printf("%.57f", 0.1 + 0.2);
將得到:
0.300000000000000044408920985006261616945266723632812500000
那我們的問題來了,為什麼計算機計算的0.1加0.2會不等於0.3?
首先我們來看一下JS能夠表示最大數是多少,如下所示,列印Number.MAX_SAFE_INTEGER和Number.MAX_VALUE:
JS能表示的最大整數為9e16,能表示的最大正數為1.79e308,這兩個數是怎麼得來的呢?先來看一下整數在計算機的存儲方式。
我們知道計算機是使用二進位存儲數據的,整數也是同樣的道理,整數可以分成短整型、基本型、長整型,佔用的存儲空間分別為16位、32位、64位,如果操作系統是32位的,那麼使用長整型將會慢於短整型,因為一個數它需要分兩次取,而在64位的操作系統,一次就可以取到8個位元組或64位的數據,所以使用長整型不會有性能問題。另外,32位的操作系統內存只能識別到2 ^ 32 = 4G,而現在的電腦內存動不動就是8G、16G,所以現在的電腦基本都是64位的,不比前幾年。
32位有符號整型的存儲方式如下圖所示:
第一位0表示正數,1表示負數,剩下的31位表示數值,所以32位有符號整數最大值為:
即21億多,如果要表示全球人口,那麼32位整型是不夠的。同理,64位有符號整型能表示的最大值為:
這是一個19位數,mysql資料庫的id欄位就經常用長整型表示,那為什麼JS能表示的最大整數只有16位,而不是19位呢?這個要先說一下浮點型在計算機的存儲方式。
現在浮點型的存儲實現基本按IEEE754標準,符點數分為單精度和雙精度,單精度為32位,雙精度為64位。
在十進位裡面,一個小數如0.75可以表示成7.5 * 10 ^ -1,同樣地在二進位裡面,0.75可以表示成:
0.75 = 1.1 * 2 ^ -1
即0.75 = (1 + 1 * 2 ^ -1) * 2 ^ -1,其中冪次方-1用階碼錶示,而1.1由於二進位整數部分都是1,所以去掉1留下0.1作為尾數部分(因為都是1點多的形式,所以這個1就沒必要存了)。因此0.75在單精度浮點數是這樣表示的:
注意階碼要加上一個基數,這個基數為2 ^ (n – 1) – 1,n為階碼的位數,32位的階碼為8位,所以這個基數為127,8位階碼能表示的最小整數為0,最大整數為255,所以能表示的指數範圍為:(0 – 127) ~ (255 – 127)即-127~128,上面要表示指數為-1,需要加上基數127,就變成126,如上圖所示。
而尾數為0.1,所以尾數的最高位為1,後面的值填充0.
反過來,如果知道一個二進位的存儲方式,同樣地可以轉換成10進位,如上圖的計算結果應為:
(1 + 1 * 2 ^ – 1) * 2 ^ (126 – 127) = 1.5 * 2 ^ -1 = 0.75
那麼0.1又該如何表示成一個二進位呢?
由於0.75 = 1 * 2 ^ -1 + 1 * 2 ^ -2,剛好可以被二進位精確表示,那0.1呢?沒辦法了,0.1無法被表示成這種形式,只能是用另外一個數儘可能地接近0.1(同理1/3無法在10進位精確表示,但是可以在3進位精確表示,只是我們習慣了10進位)。
我們可以用一小段C代碼來研究一下0.1被存儲成什麼了,如下代碼所示:
因為C可以讀取到原始的內存內容,所以可以列印每位的數據是什麼。如上代碼列印的結果如下:
雙精度浮點數用11位表示階碼,52位表示尾數,如下圖所示:
尾數約為0.6:
由於這個精度不夠,我們要找一個高精度的計算器,如筆者找的這個:
有了這個尾數之後,再讓它乘以指數,得到結果為:
也就是說0.1的實際存儲要比0.1大,大概大了5.5e-17.
注意到,0.2和0.1的區別在於0.2比0.1的階碼大了1,其它的完全一樣。所以,0.2也是偏大了:
兩個數相加的結果為:
但是注意到0.1 + 0.2並不是上面的結果,要比上面的大:
這又是為什麼呢?因為浮點數相加,需要先比較階碼是否一致,如果一致則尾數直接相加,如果不一致,需要先對階,小階往大階看齊,即把小階的指數調成和大階的一樣大,然後把它的尾數向右移相應的位數。如上面的0.1是小階,需要對它進行處理,如下:
需要把0.1的小數點向右移一位變成:
向右移一位導致尾數需要進行截斷,由於最後一位剛好是0,所以這裡直接捨棄,如果是1,那麼尾數加1,類似於十進位的四捨五入,避免誤差累積。現在0.1和0.2的階碼一樣了,尾數可以進行相加減了,如下把它們倆的尾數相加:
可以看到,發生了進位,變成了53位,已經超過了尾數52位的範圍,所以需要把階碼進一位,即指數加1,兩數和的尾數右移一位,即除以2,由於尾數的最後一位是1,進行「四捨五入」,即捨棄最後一位後再加上1,最後尾數變成了如下圖所示:
而指數加1,變成了-2,所以最後的計算結果為:
這個就和控制台的輸出一致了,並且和C的輸出完全一致。到此,我們就回答了為什麼0.1加0.2不等於0.3了。上面還提出了兩個問題,其中一個是:為什麼JS的最大正數是1.79e308呢?這個數其實就是雙精度浮點數所能表示最大正值,如下使用python的輸出:
那為什麼JS的最大正整數不是正常的64位的長整型所能表示的19位呢?因為JS的正整數是用的尾數的長度表示的,由於尾數是52位,加上整數的一位,它所能表示的最大的整數為:
為什麼JS要用這種方式呢?因為JS的整型和浮點型在計算過程中可以隨時自動切換,應該是考慮到了這個原因,所以才拿浮點型的大小限制來做整型大小的限制。
由於後端的資料庫的ID欄位可能會大於這個值,如果傳來了一個很大的數,在調JSON.parse的時候將會丟失精度,ID就不對了,所以如果出現這種情況,應該讓後端把ID當成字元串的方式傳給你。
但是會有一種情況精度要求很高,15位精度會不夠用,例如計算天體運算。那怎麼辦呢?有一種絕對精準的方式就是用分數表示,例如0.1 + 0.2 = 3/10,計算的過程和最後的結果都用分數表示,分數的結果,你需要精確到多少位都可以取到。這個在matlab/maple等科學計算軟體都有實現。
最後怎麼判斷兩個小數是否相等呢?用等號肯定是不行的了,判斷兩個小數是否相等要用它們的差值和一個很小的小數進行比較,如果小於這個小數,則認為兩者相等,ES6新增了一個Math.EPSILON屬性,如比較0.1 + 0.2是否等0.3應該這麼操作:
作者:會編程的銀豬
原文:http://www.renfed.com/2017/05/13/float-number/
---- 廣告----
![](https://pic.pimg.tw/zzuyanan/1488615166-1259157397.png)
![](https://pic.pimg.tw/zzuyanan/1482887990-2595557020.jpg)
※怎樣實現前端裁剪上傳圖片功能
※最終,JavaScript成為了一流語言
※JavaScript非同步與Promise實現
※這真的是我見過的最全的全套視頻資源
※如何看待浙江大學申請雙向綁定技術專利
TAG:JavaScript |
※6GB+驍龍660,1399元的360N6為什麼賣不好?
※360手機2017年銷量500萬,360為什麼要做手機
※為什麼1200X600瓷磚成了「萬人迷」
※小姐姐穿什麼鞋 「2018 Vol.12」
※比領克01漂亮10倍,1.6T配全景天窗,售14萬,還看什麼VV7?
※從2015到2018,小米為什麼選擇現在上市?
※「2018,遇見英國」2018年英國的熱門資產類別是什麼?
※最便宜驍龍660手機,6GB的360N6為什麼賣不好?
※今天你穿了什麼鞋之網友投稿「2018 Vol.13」
※都是天絲,40S、60S、80S有什麼區別?
※2018什麼手機續航好?10款續航能力4000mAh以上手機推薦
※每天被100000萬細菌環繞,是什麼感覺?
※小米為什麼能支撐起這1000億市值?
※夏朝距今只有4000年,為什麼我們說5000年中華文明史?
※1944年諾曼底登陸德國為什麼用450架飛機對戰盟軍13700架飛機?
※小米憑什麼估值1000億美元?蘋果表示不服!
※測試| 2018年什麼顏色旺你!
※不平等條約為什麼要簽訂99年?比100年更屈辱
※100斤和120斤真的沒什麼區別!?讓這些男神、女神的黑歷史告訴你...
※宇宙直徑有930億光年,它在什麼時候會達到1000億光年呢?