浮點數精度問題透析:小數計算不準確+浮點數精度丟失根源

在知乎上上看到如下問題:

浮點數精度問題的前世今生?

1.該問題出現的原因 ?

2.爲何其他編程語言,比如java中可能沒有js那麼明顯

3.大家在項目中踩過浮點數精度的坑?

4.最後採用哪些方案規避這個問題的?

5.爲何採用改方案?

例如在 chrome js console 中:

alert(0.7+0.1); //輸出0.7999999999999999

之前自己答的不是滿意(對 陳嘉棟的回答 還是滿意的),想對這個問題做個深入淺出的總結

再看到這幾篇長文[ JS 基礎 ] JS 浮點數四則運算精度丟失問題 (3)》、《JavaScript數字精度丟失問題總結》、《細說 JavaScript 七種數據類型,略有所悟,整理如下:

這個問題並不只是在Javascript中才會出現,任何使用二進制浮點數的編程語言都會有這個問題,只不過在 C++/C#/Java 這些語言中已經封裝好了方法來避免精度的問題,而 JavaScript 是一門弱類型的語言,從設計思想上就沒有對浮點數有個嚴格的數據類型,所以精度誤差的問題就顯得格外突出。

java浮點數丟失問題

python浮點數丟失問題

浮點數丟失產生原因

JavaScript 中的數字類型只有 Number 一種,Number 類型採用 IEEE754 標準中的 “雙精度浮點數” 來表示一個數字,不區分整數和浮點數 (js位運算或許是爲了提升B格)。

幾乎所有的編程語言浮點數都是都採用IEEE浮點數算術標準。java float 32 浮點數:  1bit符號  8bit指數部分 23bit尾數。推薦閱讀《JAVA 浮點數的範圍和精度

什麼是IEEE-745浮點數表示法

IEEE-745浮點數表示法是一種可以精確地表示分數的二進制示法,比如1/2,1/8,1/1024

十進制小數如何表示爲轉爲二進制

十進制整數轉二進制

十進制整數換成二進制一般都會:1=>1 2=>10 3=>101 4=>100 5=>101 6=>110   

6/2=3…0

3/2=1…1

1/2=0…1

倒過來就是110

十進制小數轉二進制

0.25的二進制

0.25*2=0.5 取整是0

0.5*2=1.0    取整是1

即0.25的二進制爲 0.01 ( 第一次所得到爲最高位,最後一次得到爲最低位)

0.8125的二進制

0.8125*2=1.625   取整是1

0.625*2=1.25     取整是1

0.25*2=0.5       取整是0

0.5*2=1.0        取整是1

即0.8125的二進制是0.1101(第一次所得到爲最高位,最後一次得到爲最低位)

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

0.4*2=0.8======取出整數部分0

0.8*2=1.6======取出整數部分1

0.6*2=1.2======取出整數部分1

接下來會無限循環

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.1轉化成二進制是:0.0001 1001 1001 1001…(無限循環)

0.1 => 0.0001 1001 1001 1001…(無限循環)

同理0.2的二進制是0.0011 0011 0011 0011…(無限循環)

IEEE-745浮點數表示法存儲結構

在 IEEE754 中,雙精度浮點數採用 64 位存儲,即 8 個字節表示一個浮點數 。其存儲結構如下圖所示:

指數位可以通過下面的方法轉換爲使用的指數值:

IEEE-745浮點數表示法記錄數值範圍

從存儲結構中可以看出, 指數部分的長度是11個二進制,即指數部分能表示的最大值是 2047(2^11-1)

取中間值進行偏移,用來表示負指數,也就是說指數的範圍是 [-1023,1024] 。

因此,這種存儲結構能夠表示的數值範圍爲 2^1024 到 2^-1023 ,超出這個範圍的數無法表示 。2^1024  和 2^-1023  轉換爲科學計數法如下所示:

1.7976931348623157 × 10^308

5 × 10^-324

因此,JavaScript 中能表示的最大值是 1.7976931348623157 × 10308,最小值爲 5 × 10-324 。java雙精度類型 double也是如此。

這兩個邊界值可以分別通過訪問 Number 對象的 MAX_VALUE 屬性和 MIN_VALUE 屬性來獲取:

Number.MAX_VALUE; // 1.7976931348623157e+308
Number.MIN_VALUE; // 5e-324

如果數字超過最大值或最小值,JavaScript 將返回一個不正確的值,這稱爲 “正向溢出(overflow)” 或 “負向溢出(underflow)” 。 

Number.MAX_VALUE+1 == Number.MAX_VALUE; //true
Number.MAX_VALUE+1e292; //Infinity
Number.MIN_VALUE + 1; //1
Number.MIN_VALUE - 3e-324; //0
Number.MIN_VALUE - 2e-324; //5e-324

IEEE-745浮點數表示法數值精度

在 64 位的二進制中,符號位決定了一個數的正負,指數部分決定了數值的大小,小數部分決定了數值的精度

IEEE754 規定,有效數字第一位默認總是1 。因此,在表示精度的位數前面,還存在一個 “隱藏位” ,固定爲 1 ,但它不保存在 64 位浮點數之中。也就是說,有效數字總是 1.xx...xx 的形式,其中 xx..xx 的部分保存在 64 位浮點數之中,最長爲52位 。所以,JavaScript 提供的有效數字最長爲 53 個二進制位,其內部實際的表現形式爲:

(-1)^符號位 * 1.xx...xx * 2^指數位

這意味着,JavaScript 能表示並進行精確算術運算的整數範圍爲:[-2^53-1,2^53-1],即從最小值 -9007199254740991 到最大值 9007199254740991 之間的範圍 。

Math.pow(2, 53)-1 ; // 9007199254740991
-Math.pow(2, 53)-1 ; // -9007199254740991

可以通過 Number.MAX_SAFE_INTEGER 和  Number.MIN_SAFE_INTEGER 來分別獲取這個最大值和最小值。 

console.log(Number.MAX_SAFE_INTEGER) ; // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER) ; // -9007199254740991

對於超過這個範圍的整數,JavaScript 依舊可以進行運算,但卻不保證運算結果的精度。

Math.pow(2, 53) ; // 9007199254740992
Math.pow(2, 53) + 1; // 9007199254740992
9007199254740993; //9007199254740992
90071992547409921; //90071992547409920
0.923456789012345678;//0.9234567890123456

IEEE-745浮點數表示法數值精度丟失

計算機中的數字都是以二進制存儲的,二進制浮點數表示法並不能精確的表示類似0.1這樣 的簡單的數字

如果要計算 0.1 + 0.2 的結果,計算機會先把 0.1 和 0.2 分別轉化成二進制,然後相加,最後再把相加得到的結果轉爲十進制 

但有一些浮點數在轉化爲二進制時,會出現無限循環 。比如, 十進制的 0.1 轉化爲二進制,會得到如下結果:

0.1 => 0.0001 1001 1001 1001…(無限循環)

0.2 => 0.0011 0011 0011 0011…(無限循環)

而存儲結構中的尾數部分最多隻能表示 53 位。爲了能表示 0.1,只能模仿十進制進行四捨五入了,但二進制只有 0 和 1 , 於是變爲 0 舍 1 入 。 因此,0.1 在計算機裏的二進制表示形式如下:

0.1 => 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 101

0.2 => 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 001

用標準計數法表示如下:

0.1 => (−1)0 × 2^4 × (1.1001100110011001100110011001100110011001100110011010)2

0.2 => (−1)0 × 2^3 × (1.1001100110011001100110011001100110011001100110011010)2 

在計算浮點數相加時,需要先進行 “對位”,將較小的指數化爲較大的指數,並將小數部分相應右移:

最終,“0.1 + 0.2” 在計算機裏的計算過程如下:

經過上面的計算過程,0.1 + 0.2 得到的結果也可以表示爲:

(−1)0 × 2−2 × (1.0011001100110011001100110011001100110011001100110100)2=>.0.30000000000000004

通過 JS 將這個二進制結果轉化爲十進制表示:

(-1)**0 * 2**-2 * (0b10011001100110011001100110011001100110011001100110100 * 2**-52); //0.30000000000000004

console.log(0.1 + 0.2) ; // 0.30000000000000004

這是一個典型的精度丟失案例,從上面的計算過程可以看出,0.1 和 0.2 在轉換爲二進制時就發生了一次精度丟失,而對於計算後的二進制又有一次精度丟失 。因此,得到的結果是不準確的。

浮點數丟失解決方案

我們常用的分數(特別是在金融的計算方面)都是十進制分數1/10,1/100等。或許以後電路設計或許會支持十進制數字類型以避免這些舍入問題。在這之前,你更願意使用大整數進行重要的金融計算,例如,要使用整數‘分’而不是使用小數‘元’進行貨比單位的運算

即在運算前我們把參加運算的數先升級(10的X的次方)到整數,等運算完後再降級(0.1的X的次方)。

在java裏面有BigDecimal庫,js裏面有big.js js-big-decimal.js。當然BCD編碼就是爲了十進制高精度運算量制。

BCD編碼

BCD編碼(一般指8421BCD碼形式)亦稱二進碼十進數或二-十進制代碼。用4位二進制數來表示1位十進制數中的0~9這10個數。一般用於高精度計算。比如會計制度經常需要對很長的數字串作準確的計算。相對於一般的浮點式記數法,採用BCD碼,既可保存數值的精確度,又可免去使電腦作浮點運算時所耗費的時間

爲什麼採用二進制

  1. 二進制在電路設計中物理上更易實現,因爲電子器件大多具有兩種穩定狀態,比如晶體管的導通和截止,電壓的高和低,磁性的有和無等。而找到一個具有十個穩定狀態的電子器件是很困難的。

  2. 二進制規則簡單,十進制有55種求和與求積的運算規則,二進制僅有各有3種,這樣可以簡化運算器等物理器件的設計。另外,計算機的部件狀態少,可以增強整個系統的穩定性。

  3. 與邏輯量相吻合。二進制數0和1正好與邏輯量“真”和“假”相對應,因此用二進制數表示二值邏輯顯得十分自然。

  4. 可靠性高。二進制中只使用0和1兩個數字,傳輸和處理時不易出錯,因而可以保障計算機具有很高的可靠性

我覺得主要還是因爲第一條。如果比如能夠設計出十進制的元器件,那麼對於設計其運算器也不再話下。

JS數字精度丟失的一些典型問題

兩個簡單的浮點數相加

0.1 + 0.2 != 0.3 // true

toFixed 不會四捨五入(Chrome)

1.335.toFixed(2) // 1.33


再問問一個問題 :在js數字類型中浮點數的最高精度多少位小數?(16位 or 17位?……why?

  1. JavaScript 能表示並進行精確算術運算的整數範圍爲:[-2^53-1,2^53-1],即從最小值 -9007199254740991 到最大值 9007199254740991 之間的範圍。'9007199254740991'.length//16 

  2. IEEE754 規定,有效數字第一位默認總是1 。因此,在表示精度的位數前面,還存在一個 “隱藏位” ,固定爲 1 ,但它不保存在 64 位浮點數之中。也就是說,有效數字總是 1.xx...xx 的形式,其中 xx..xx 的部分保存在 64 位浮點數之中,最長爲52位 。所以,JavaScript 提供的有效數字最長爲 53 個二進制位

let a=1/3

a.toString();//"0.3333333333333333"

a.toString();.length//18

a*3===0.3333333333333333*3===1

0.3333333333333332*3!==1


相關鏈接:  

http://0.30000000000000004.com

http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

浮點數精度問題透析:小數計算不準確+浮點數精度丟失根源 - computer science - 周陸軍的個人網站 如有不妥之處,請到本人源站留言。不是更新。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章