IEEE 754標准


from:https://www.jianshu.com/p/7c636d8f18d5

由於不同機器所選用的基數、尾數位長度和階碼位長度不同,因此對浮點數的表示有較大差別,這不利於軟件在不同計算機之間的移植。為此,美國IEEE(電器及電子工程師協會)提出了一個從系統角度支持浮點數的表示方法,稱為IEEE754標准(IEEE,1985),當今流行的計算機幾乎都采用了這一標准。

Java中浮點數,既float和double,都是采用的IEEE754標准。無論在java python javaScript里面都存在 1 + 2 != 3 問題,這個問題的產生根源在於計算存儲數字是二進制,對無限循環小數和無理數采用雙精度64位double浮點數_float為32位,即52位小數+11位指數+1位符號。超過52位小數溢出而產生精度丟失。

例如在 chrome js console 中:

// 加法

0.1 + 0.2 = 0.30000000000000004

0.1 + 0.7 = 0.7999999999999999

0.2 + 0.4 = 0.6000000000000001

// 減法

0.3 - 0.2 = 0.09999999999999998

1.5 - 1.2 = 0.30000000000000004

// 乘法

0.8 * 3 = 2.4000000000000004

19.9 * 100 = 1989.9999999999998

// 除法

0.3 / 0.1 = 2.9999999999999996

0.69 / 10 = 0.06899999999999999

// 比較

0.1 + 0.2 === 0.3 // false

(0.3 - 0.2) === (0.2 - 0.1) // false

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

 
 
 
 

浮點數丟失產生原因

JavaScript 中的數字類型只有 Number 一種,Number 類型采用 IEEE754 標准中的 “雙精度浮點數” 來表示一個數字,不區分整數和浮點數。

什么是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…(無限循環)

科普科學計數法

科學記數法是一種把一個數表示成a與10的n次冪相乘的形式(1≤a<10,n為整數)的記數法。

例如:19971400000000=1.99714×10^13。計算器或電腦表達10的冪是一般是用E或e,也就是1.99714E13=19971400000000。

科學記數法的形式是由兩個數的乘積組成的。表示為a×10^b(aEb),其中一個因數為a(1≤|a|<10),另一個因數為10^n。

科學計數法的精確度

運用科學記數法a×10^n的數字,它的精確度以a的最后一個數在原數中的數位為准。

13600,精確到十位,記作:1.360X10^4

13600 ,精確到百位,記作:1.36X10^4

13600,精確到千位,記作:1.3X10^4

十進制的5.0,寫成二進制是101.0,相當於1.01×2^2

十進制的-5.0,寫成二進制是-101.0,相當於-1.01×2^2,推薦閱讀《浮點數的二進制表示 

在二進制里面,即a×2^b,1≤a<2,也就是說,a可以寫成1.xxxxxx的形式,其中xxxxxx表示小數部分。IEEE 754規定,在計算機內部保存a時,默認這個數的第一位總是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的時候,只保存01,等到讀取的時候,再把第一位的1加上去。這樣做的目的,是節省1位有效數字。以64位浮點數為例,留給a只有52位,將第一位的1舍去以后,等於可以保存53位有效數字

所以js浮點數表示的數字為[-1*2^53*2^1023,1*2^53*2^1024] ,±1 為符號位,2^53 為小數位(53-1), 2^1024 1024位指數位(1024*2=2048=2^11為指數位)

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 × 10^308,最小值為 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 在轉換為二進制時就發生了一次精度丟失,而對於計算后的二進制又有一次精度丟失 。因此,得到的結果是不准確的。

幾乎所有的編程語言浮點數都是都采用IEEE浮點數算術標准

在C或者java里面,double是64位浮點數,float 是32 浮點數:  1bit符號  8bit指數部分 23bit尾數。推薦閱讀《JAVA 浮點數的范圍和精度

long與double在java中本身都是用64位存儲的,但是他們的存儲方式不同,導致double可儲存的范圍比long大很多

long可以准確存儲19位數字,而double只能准備存儲16位數字(實際測試,是17位,)。double由於有exp位,可以存16位以上的數字,但是需要以低位的不精確作為代價。如果一個大於17位的long型數字存到double上,就會丟失數字末尾的精度

如果需要高於19位數字的精確存儲,則必須用BigInteger來保存,當然會犧牲一些性能。

java 基本數據類型

基本類型存儲需求位數bit數取值范圍取值范圍

byte1byte8bit1*8(8)-2^7~2^7-1-128~127

short2byte16bit2*8(16)-2^15~2^15-1-32768~32767

int4byte32bit4*8(32)-2^31~2^31-1-2147483648~2147483647

long8byte64bit8*8(64)-2^63~2^63-1=-9223372036854775808~9223372036854775807

float4byte32bit4*8(32)3.4028235E38~1.4E-453.4028235*10^38~1.4*10^-45

double8byte64bit8*8(64)-2^1023~2^10241.7976931348623157*10^308~4.9*10^-324

char2byte16bit2*8(16)2^16 - 1unicode編碼范圍

 java中char類型占2個字節、16位可以存放漢子,字母和數字占一個字節,一個字節8位,中文占2個字節,16位。

在表中,long最大為=2^63-1,而double為2^1024,

java中int和float都是32位,long和double都是64位。為什么float和double表示的范圍大那么多呢?因為double采用IEEE-745浮點數表示法

double是n*2^m(n乘以2的m次方)這種形式存儲的,只需要記錄n和m兩個數就行了,m的值影響范圍大,所以表示的范圍比long大。

但是m越大,n的精度就越小,所以double並不能把它所表示的范圍里的所有數都能精確表示出來,而long就可以。

float浮點數,小數點后第7位是部分准確的。例如,1.0000004就是1.00000035通過得到的,其實際保存和1.0000003相同。1.0000006也是通過舍入得到的。再往前第6位及以后均可以通過小數准確表示出來。通常說float數據的有效位是6~7位,也是這個原因。一般來說,無論是整數或者小數,用float表示時,從左邊第一個非0的數字算起,從高到低的7位是准確的。此后的數位是不能保證精確的

double雙精度浮點數小數部分有52位,和上面類似,最低6位(2^-52,2^-51,......)表示的規格化小數如下所示。從圖中可以看出,雙精度浮點數能准確表示到小數點后第15位,第16位部分准確。用double表示時,從左邊第一個非0的數字起,從高到低的16位是准確的,此后的數位不一定精確

盡管浮點數表示的范圍很廣,但由於精度損失的存在,加上冪次的放大作用,一個浮點數實際上是表示了周圍的一個有理數區間。如果將浮點數繪制到一個數軸上,直觀上看,靠近0的部分,浮點數出現較密集。越靠近無窮大,浮點數分布越稀疏,一個浮點值代表了周圍一片數據。如下圖所示。從這個意義上來說,浮點數不宜直接比較相等,它們是代表了一個數據范圍。實際應用中,如果要使用浮點數計算,一定要考慮精度問題。在滿足精度要求的前提下,計算結果才是有效的。 

在計算精度要求情形下,例如商業計算等,應該避免使用浮點數,嚴格采取高精度計算。

 
 

浮點數丟失解決方案

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

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

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

BCD編碼

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

為什么采用二進制

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

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

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

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

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

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

兩個簡單的浮點數相加

0.1 + 0.2 != 0.3 // true

toFixed 不會四舍五入(Chrome)

1.335.toFixed(2) // 1.33

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

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

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



作者:ksfkf
鏈接:https://www.jianshu.com/p/7c636d8f18d5
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM