浮點(diǎn)數(shù)精度問題透析:小數(shù)計(jì)算不準(zhǔn)確+浮點(diǎn)數(shù)精度丟失根源

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

浮點(diǎn)數(shù)精度問題的前世今生?

1.該問題出現(xiàn)的原因 ?

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

3.大家在項(xiàng)目中踩過浮點(diǎn)數(shù)精度的坑?

4.最后采用哪些方案規(guī)避這個(gè)問題的?

5.為何采用改方案?

例如在 chrome js console 中:

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

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

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

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

浮點(diǎn)數(shù)丟失產(chǎn)生原因

JavaScript 中的數(shù)字類型只有 Number 一種,Number 類型采用 IEEE754 標(biāo)準(zhǔn)中的 “雙精度浮點(diǎn)數(shù)” 來表示一個(gè)數(shù)字,不區(qū)分整數(shù)和浮點(diǎn)數(shù)?(js位運(yùn)算或許是為了提升B格)。

幾乎所有的編程語言浮點(diǎn)數(shù)都是都采用IEEE浮點(diǎn)數(shù)算術(shù)標(biāo)準(zhǔn)。java float 32 浮點(diǎn)數(shù):? 1bit符號? 8bit指數(shù)部分 23bit尾數(shù)。推薦閱讀《JAVA 浮點(diǎn)數(shù)的范圍和精度

什么是IEEE-745浮點(diǎn)數(shù)表示法

IEEE-745浮點(diǎn)數(shù)表示法是一種可以精確地表示分?jǐn)?shù)的二進(jìn)制示法,比如1/2,1/8,1/1024

十進(jìn)制小數(shù)如何表示為轉(zhuǎn)為二進(jìn)制

十進(jìn)制整數(shù)轉(zhuǎn)二進(jìn)制

十進(jìn)制整數(shù)換成二進(jìn)制一般都會(huì):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

十進(jìn)制小數(shù)轉(zhuǎn)二進(jìn)制

0.25的二進(jìn)制

0.25*2=0.5 取整是0

0.5*2=1.0? ? 取整是1

即0.25的二進(jìn)制為 0.01 ( 第一次所得到為最高位,最后一次得到為最低位)

0.8125的二進(jìn)制

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的二進(jìn)制是0.1101(第一次所得到為最高位,最后一次得到為最低位)

0.1的二進(jìn)制

0.1*2=0.2======取出整數(shù)部分0

0.2*2=0.4======取出整數(shù)部分0

0.4*2=0.8======取出整數(shù)部分0

0.8*2=1.6======取出整數(shù)部分1

0.6*2=1.2======取出整數(shù)部分1

0.2*2=0.4======取出整數(shù)部分0

0.4*2=0.8======取出整數(shù)部分0

0.8*2=1.6======取出整數(shù)部分1

0.6*2=1.2======取出整數(shù)部分1

接下來會(huì)無限循環(huán)

0.2*2=0.4======取出整數(shù)部分0

0.4*2=0.8======取出整數(shù)部分0

0.8*2=1.6======取出整數(shù)部分1

0.6*2=1.2======取出整數(shù)部分1

所以0.1轉(zhuǎn)化成二進(jìn)制是:0.0001 1001 1001 1001…(無限循環(huán))

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

同理0.2的二進(jìn)制是0.0011 0011 0011 0011…(無限循環(huán))

IEEE-745浮點(diǎn)數(shù)表示法存儲(chǔ)結(jié)構(gòu)

在 IEEE754 中,雙精度浮點(diǎn)數(shù)采用 64 位存儲(chǔ),即 8 個(gè)字節(jié)表示一個(gè)浮點(diǎn)數(shù) 。其存儲(chǔ)結(jié)構(gòu)如下圖所示:

指數(shù)位可以通過下面的方法轉(zhuǎn)換為使用的指數(shù)值:

IEEE-745浮點(diǎn)數(shù)表示法記錄數(shù)值范圍

從存儲(chǔ)結(jié)構(gòu)中可以看出, 指數(shù)部分的長度是11個(gè)二進(jìn)制,即指數(shù)部分能表示的最大值是 2047(2^11-1)

取中間值進(jìn)行偏移,用來表示負(fù)指數(shù),也就是說指數(shù)的范圍是 [-1023,1024] 。

因此,這種存儲(chǔ)結(jié)構(gòu)能夠表示的數(shù)值范圍為 2^1024 到 2^-1023 ,超出這個(gè)范圍的數(shù)無法表示 。2^1024? 和 2^-1023? 轉(zhuǎn)換為科學(xué)計(jì)數(shù)法如下所示:

1.7976931348623157 × 10^308

5 × 10^-324

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

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

Number.MAX_VALUE;?//?1.7976931348623157e+308

Number.MIN_VALUE;?//?5e-324

如果數(shù)字超過最大值或最小值,JavaScript 將返回一個(gè)不正確的值,這稱為 “正向溢出(overflow)” 或 “負(fù)向溢出(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浮點(diǎn)數(shù)表示法數(shù)值精度

在 64 位的二進(jìn)制中,符號位決定了一個(gè)數(shù)的正負(fù),指數(shù)部分決定了數(shù)值的大小,小數(shù)部分決定了數(shù)值的精度

IEEE754 規(guī)定,有效數(shù)字第一位默認(rèn)總是1 。因此,在表示精度的位數(shù)前面,還存在一個(gè) “隱藏位” ,固定為 1 ,但它不保存在 64 位浮點(diǎn)數(shù)之中。也就是說,有效數(shù)字總是 1.xx...xx 的形式,其中 xx..xx 的部分保存在 64 位浮點(diǎn)數(shù)之中,最長為52位 。所以,JavaScript 提供的有效數(shù)字最長為 53 個(gè)二進(jìn)制位,其內(nèi)部實(shí)際的表現(xiàn)形式為:

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

這意味著,JavaScript 能表示并進(jìn)行精確算術(shù)運(yùn)算的整數(shù)范圍為:[-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 來分別獲取這個(gè)最大值和最小值。?

console.log(Number.MAX_SAFE_INTEGER)?;?//?9007199254740991

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

對于超過這個(gè)范圍的整數(shù),JavaScript 依舊可以進(jìn)行運(yùn)算,但卻不保證運(yùn)算結(jié)果的精度。

Math.pow(2,?53)?;?//?9007199254740992

Math.pow(2,?53)?+?1;?//?9007199254740992

9007199254740993;?//9007199254740992

90071992547409921;?//90071992547409920

0.923456789012345678;//0.9234567890123456

IEEE-745浮點(diǎn)數(shù)表示法數(shù)值精度丟失

計(jì)算機(jī)中的數(shù)字都是以二進(jìn)制存儲(chǔ)的,二進(jìn)制浮點(diǎn)數(shù)表示法并不能精確的表示類似0.1這樣 的簡單的數(shù)字

如果要計(jì)算 0.1 + 0.2 的結(jié)果,計(jì)算機(jī)會(huì)先把 0.1 和 0.2 分別轉(zhuǎn)化成二進(jìn)制,然后相加,最后再把相加得到的結(jié)果轉(zhuǎn)為十進(jìn)制?

但有一些浮點(diǎn)數(shù)在轉(zhuǎn)化為二進(jìn)制時(shí),會(huì)出現(xiàn)無限循環(huán) 。比如, 十進(jìn)制的 0.1 轉(zhuǎn)化為二進(jìn)制,會(huì)得到如下結(jié)果:

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

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

而存儲(chǔ)結(jié)構(gòu)中的尾數(shù)部分最多只能表示 53 位。為了能表示 0.1,只能模仿十進(jìn)制進(jìn)行四舍五入了,但二進(jìn)制只有 0 和 1 , 于是變?yōu)?0 舍 1 入 。 因此,0.1 在計(jì)算機(jī)里的二進(jìn)制表示形式如下:

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

用標(biāo)準(zhǔn)計(jì)數(shù)法表示如下:

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

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

在計(jì)算浮點(diǎn)數(shù)相加時(shí),需要先進(jìn)行 “對位”,將較小的指數(shù)化為較大的指數(shù),并將小數(shù)部分相應(yīng)右移:

最終,“0.1 + 0.2” 在計(jì)算機(jī)里的計(jì)算過程如下:

經(jīng)過上面的計(jì)算過程,0.1 + 0.2 得到的結(jié)果也可以表示為:

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

通過 JS 將這個(gè)二進(jìn)制結(jié)果轉(zhuǎn)化為十進(jìn)制表示:

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

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

這是一個(gè)典型的精度丟失案例,從上面的計(jì)算過程可以看出,0.1 和 0.2 在轉(zhuǎn)換為二進(jìn)制時(shí)就發(fā)生了一次精度丟失,而對于計(jì)算后的二進(jìn)制又有一次精度丟失 。因此,得到的結(jié)果是不準(zhǔn)確的。

浮點(diǎn)數(shù)丟失解決方案

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

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

在java里面有BigDecimal庫,js里面有big.jsjs-big-decimal.js。當(dāng)然BCD編碼就是為了十進(jìn)制高精度運(yùn)算量制。

BCD編碼

BCD編碼(一般指8421BCD碼形式)亦稱二進(jìn)碼十進(jìn)數(shù)或二-十進(jìn)制代碼。用4位二進(jìn)制數(shù)來表示1位十進(jìn)制數(shù)中的0~9這10個(gè)數(shù)。一般用于高精度計(jì)算。比如會(huì)計(jì)制度經(jīng)常需要對很長的數(shù)字串作準(zhǔn)確的計(jì)算。相對于一般的浮點(diǎn)式記數(shù)法,采用BCD碼,既可保存數(shù)值的精確度,又可免去使電腦作浮點(diǎn)運(yùn)算時(shí)所耗費(fèi)的時(shí)間。

為什么采用二進(jìn)制

二進(jìn)制在電路設(shè)計(jì)中物理上更易實(shí)現(xiàn),因?yàn)殡娮悠骷蠖嗑哂袃煞N穩(wěn)定狀態(tài),比如晶體管的導(dǎo)通和截止,電壓的高和低,磁性的有和無等。而找到一個(gè)具有十個(gè)穩(wěn)定狀態(tài)的電子器件是很困難的。

二進(jìn)制規(guī)則簡單,十進(jìn)制有55種求和與求積的運(yùn)算規(guī)則,二進(jìn)制僅有各有3種,這樣可以簡化運(yùn)算器等物理器件的設(shè)計(jì)。另外,計(jì)算機(jī)的部件狀態(tài)少,可以增強(qiáng)整個(gè)系統(tǒng)的穩(wěn)定性。

與邏輯量相吻合。二進(jìn)制數(shù)0和1正好與邏輯量“真”和“假”相對應(yīng),因此用二進(jìn)制數(shù)表示二值邏輯顯得十分自然。

可靠性高。二進(jìn)制中只使用0和1兩個(gè)數(shù)字,傳輸和處理時(shí)不易出錯(cuò),因而可以保障計(jì)算機(jī)具有很高的可靠性

我覺得主要還是因?yàn)榈谝粭l。如果比如能夠設(shè)計(jì)出十進(jìn)制的元器件,那么對于設(shè)計(jì)其運(yùn)算器也不再話下。

JS數(shù)字精度丟失的一些典型問題

兩個(gè)簡單的浮點(diǎn)數(shù)相加

0.1 + 0.2 != 0.3 // true

toFixed 不會(huì)四舍五入(Chrome)

1.335.toFixed(2) // 1.33

再問問一個(gè)問題 :在js數(shù)字類型中浮點(diǎn)數(shù)的最高精度多少位小數(shù)?(16位 or 17位?……why?

JavaScript 能表示并進(jìn)行精確算術(shù)運(yùn)算的整數(shù)范圍為:[-2^53-1,2^53-1],即從最小值 -9007199254740991 到最大值 9007199254740991 之間的范圍。'9007199254740991'.length//16?

IEEE754 規(guī)定,有效數(shù)字第一位默認(rèn)總是1 。因此,在表示精度的位數(shù)前面,還存在一個(gè) “隱藏位” ,固定為 1 ,但它不保存在 64 位浮點(diǎn)數(shù)之中。也就是說,有效數(shù)字總是 1.xx...xx 的形式,其中 xx..xx 的部分保存在 64 位浮點(diǎn)數(shù)之中,最長為52位 。所以,JavaScript 提供的有效數(shù)字最長為 53 個(gè)二進(jìn)制位

let a=1/3

a.toString();//"0.3333333333333333"

a.toString();.length//18

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

0.3333333333333332*3!==1

相關(guān)鏈接:??

http://0.30000000000000004.com

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

浮點(diǎn)數(shù)精度問題透析:小數(shù)計(jì)算不準(zhǔn)確+浮點(diǎn)數(shù)精度丟失根源 - computer science - 周陸軍的個(gè)人網(wǎng)站 如有不妥之處,請到本人源站留言。不是更新。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容