由于不同機器所選用的基數(shù)、尾數(shù)位長度和階碼位長度不同,因此對浮點數(shù)的表示有較大差別,這不利于軟件在不同計算機之間的移植。為此,美國IEEE(電器及電子工程師協(xié)會)提出了一個從系統(tǒng)角度支持浮點數(shù)的表示方法,稱為IEEE754標(biāo)準(zhǔn)(IEEE,1985),當(dāng)今流行的計算機幾乎都采用了這一標(biāo)準(zhǔn)。
Java中浮點數(shù),既float和double,都是采用的IEEE754標(biāo)準(zhǔn)。無論在java python javaScript里面都存在 1 + 2 != 3 問題,這個問題的產(chǎn)生根源在于計算存儲數(shù)字是二進(jìn)制,對無限循環(huán)小數(shù)和無理數(shù)采用雙精度64位double浮點數(shù)_float為32位,即52位小數(shù)+11位指數(shù)+1位符號。超過52位小數(shù)溢出而產(chǎn)生精度丟失。
例如在 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中才會出現(xiàn),任何使用二進(jìn)制浮點數(shù)的編程語言都會有這個問題,只不過在 C++/C#/Java 這些語言中已經(jīng)封裝好了方法來避免精度的問題,而 JavaScript 是一門弱類型的語言,從設(shè)計思想上就沒有對浮點數(shù)有個嚴(yán)格的數(shù)據(jù)類型,所以精度誤差的問題就顯得格外突出。
浮點數(shù)丟失產(chǎn)生原因
JavaScript 中的數(shù)字類型只有 Number 一種,Number 類型采用 IEEE754 標(biāo)準(zhǔn)中的 “雙精度浮點數(shù)” 來表示一個數(shù)字,不區(qū)分整數(shù)和浮點數(shù)。
什么是IEEE-745浮點數(shù)表示法
IEEE-745浮點數(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)制一般都會: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á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))
科普科學(xué)計數(shù)法
科學(xué)記數(shù)法是一種把一個數(shù)表示成a與10的n次冪相乘的形式(1≤a<10,n為整數(shù))的記數(shù)法。
例如:19971400000000=1.99714×10^13。計算器或電腦表達(dá)10的冪是一般是用E或e,也就是1.99714E13=19971400000000。
科學(xué)記數(shù)法的形式是由兩個數(shù)的乘積組成的。表示為a×10^b(aEb),其中一個因數(shù)為a(1≤|a|<10),另一個因數(shù)為10^n。
科學(xué)計數(shù)法的精確度
運用科學(xué)記數(shù)法a×10^n的數(shù)字,它的精確度以a的最后一個數(shù)在原數(shù)中的數(shù)位為準(zhǔn)。
13600,精確到十位,記作:1.360X10^4
13600 ,精確到百位,記作:1.36X10^4
13600,精確到千位,記作:1.3X10^4
十進(jìn)制的5.0,寫成二進(jìn)制是101.0,相當(dāng)于1.01×2^2
十進(jìn)制的-5.0,寫成二進(jìn)制是-101.0,相當(dāng)于-1.01×2^2,推薦閱讀《浮點數(shù)的二進(jìn)制表示?》
在二進(jìn)制里面,即a×2^b,1≤a<2,也就是說,a可以寫成1.xxxxxx的形式,其中xxxxxx表示小數(shù)部分。IEEE 754規(guī)定,在計算機內(nèi)部保存a時,默認(rèn)這個數(shù)的第一位總是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的時候,只保存01,等到讀取的時候,再把第一位的1加上去。這樣做的目的,是節(jié)省1位有效數(shù)字。以64位浮點數(shù)為例,留給a只有52位,將第一位的1舍去以后,等于可以保存53位有效數(shù)字
所以js浮點數(shù)表示的數(shù)字為[-1*2^53*2^1023,1*2^53*2^1024] ,±1 為符號位,2^53 為小數(shù)位(53-1), 2^1024 1024位指數(shù)位(1024*2=2048=2^11為指數(shù)位)
IEEE-745浮點數(shù)表示法存儲結(jié)構(gòu)
在 IEEE754 中,雙精度浮點數(shù)采用 64 位存儲,即 8 個字節(jié)表示一個浮點數(shù) 。其存儲結(jié)構(gòu)如下圖所示:
指數(shù)位可以通過下面的方法轉(zhuǎn)換為使用的指數(shù)值:
IEEE-745浮點數(shù)表示法記錄數(shù)值范圍
從存儲結(jié)構(gòu)中可以看出, 指數(shù)部分的長度是11個二進(jìn)制,即指數(shù)部分能表示的最大值是 2047(2^11-1)
取中間值進(jìn)行偏移,用來表示負(fù)指數(shù),也就是說指數(shù)的范圍是 [-1023,1024]?
因此,這種存儲結(jié)構(gòu)能夠表示的數(shù)值范圍為 2^1024 到 2^-1023 ,超出這個范圍的數(shù)無法表示 。2^1024? 和 2^-1023? 轉(zhuǎn)換為科學(xué)計數(shù)法如下所示:
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
如果數(shù)字超過最大值或最小值,JavaScript 將返回一個不正確的值,這稱為 “正向溢出(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浮點數(shù)表示法數(shù)值精度
在 64 位的二進(jìn)制中,符號位決定了一個數(shù)的正負(fù),指數(shù)部分決定了數(shù)值的大小,小數(shù)部分決定了數(shù)值的精度。
IEEE754 規(guī)定,有效數(shù)字第一位默認(rèn)總是1 。因此,在表示精度的位數(shù)前面,還存在一個 “隱藏位” ,固定為 1 ,但它不保存在 64 位浮點數(shù)之中。也就是說,有效數(shù)字總是 1.xx...xx 的形式,其中 xx..xx 的部分保存在 64 位浮點數(shù)之中,最長為52位 。所以,JavaScript 提供的有效數(shù)字最長為 53 個二進(jìn)制位,其內(nèi)部實際的表現(xiàn)形式為:
(-1)^符號位 * 1.xx...xx * 2^指數(shù)位
這意味著,JavaScript 能表示并進(jìn)行精確算術(shù)運算的整數(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 來分別獲取這個最大值和最小值。?
console.log(Number.MAX_SAFE_INTEGER)?;?//?9007199254740991
console.log(Number.MIN_SAFE_INTEGER)?;?//?-9007199254740991
對于超過這個范圍的整數(shù),JavaScript 依舊可以進(jìn)行運算,但卻不保證運算結(jié)果的精度。
Math.pow(2,?53)?;?//?9007199254740992
Math.pow(2,?53)?+?1;?//?9007199254740992
9007199254740993;?//9007199254740992
90071992547409921;?//90071992547409920
0.923456789012345678;//0.9234567890123456
IEEE-745浮點數(shù)表示法數(shù)值精度丟失
計算機中的數(shù)字都是以二進(jìn)制存儲的,二進(jìn)制浮點數(shù)表示法并不能精確的表示類似0.1這樣 的簡單的數(shù)字
如果要計算 0.1 + 0.2 的結(jié)果,計算機會先把 0.1 和 0.2 分別轉(zhuǎn)化成二進(jìn)制,然后相加,最后再把相加得到的結(jié)果轉(zhuǎn)為十進(jìn)制?
但有一些浮點數(shù)在轉(zhuǎn)化為二進(jìn)制時,會出現(xiàn)無限循環(huán) 。比如, 十進(jìn)制的 0.1 轉(zhuǎn)化為二進(jìn)制,會得到如下結(jié)果:
0.1 => 0.0001 1001 1001 1001…(無限循環(huán))
0.2 => 0.0011 0011 0011 0011…(無限循環(huán))
而存儲結(jié)構(gòu)中的尾數(shù)部分最多只能表示 53 位。為了能表示 0.1,只能模仿十進(jìn)制進(jìn)行四舍五入了,但二進(jìn)制只有 0 和 1 , 于是變?yōu)?0 舍 1 入 。 因此,0.1 在計算機里的二進(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)計數(shù)法表示如下:
0.1 => (?1)0 × 2^4 × (1.1001100110011001100110011001100110011001100110011010)2
0.2 =>?(?1)0 × 2^3 × (1.1001100110011001100110011001100110011001100110011010)2?
在計算浮點數(shù)相加時,需要先進(jìn)行 “對位”,將較小的指數(shù)化為較大的指數(shù),并將小數(shù)部分相應(yīng)右移:
最終,“0.1 + 0.2” 在計算機里的計算過程如下:
經(jīng)過上面的計算過程,0.1 + 0.2 得到的結(jié)果也可以表示為:
(?1)0 × 2?2 × (1.0011001100110011001100110011001100110011001100110100)2=>.0.30000000000000004
通過 JS 將這個二進(jìn)制結(jié)果轉(zhuǎn)化為十進(jìn)制表示:
(-1)**0 * 2**-2 * (0b10011001100110011001100110011001100110011001100110100 * 2**-52); //0.30000000000000004
console.log(0.1 + 0.2) ; // 0.30000000000000004
這是一個典型的精度丟失案例,從上面的計算過程可以看出,0.1 和 0.2 在轉(zhuǎn)換為二進(jìn)制時就發(fā)生了一次精度丟失,而對于計算后的二進(jìn)制又有一次精度丟失 。因此,得到的結(jié)果是不準(zhǔn)確的。
幾乎所有的編程語言浮點數(shù)都是都采用IEEE浮點數(shù)算術(shù)標(biāo)準(zhǔn)
在C或者java里面,double是64位浮點數(shù),float 是32 浮點數(shù):? 1bit符號? 8bit指數(shù)部分 23bit尾數(shù)。推薦閱讀《JAVA 浮點數(shù)的范圍和精度》
long與double在java中本身都是用64位存儲的,但是他們的存儲方式不同,導(dǎo)致double可儲存的范圍比long大很多
long可以準(zhǔn)確存儲19位數(shù)字,而double只能準(zhǔn)備存儲16位數(shù)字(實際測試,是17位,)。double由于有exp位,可以存16位以上的數(shù)字,但是需要以低位的不精確作為代價。如果一個大于17位的long型數(shù)字存到double上,就會丟失數(shù)字末尾的精度
如果需要高于19位數(shù)字的精確存儲,則必須用BigInteger來保存,當(dāng)然會犧牲一些性能。
java 基本數(shù)據(jù)類型
基本類型存儲需求位數(shù)bit數(shù)取值范圍取值范圍
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個字節(jié)、16位可以存放漢子,字母和數(shù)字占一個字節(jié),一個字節(jié)8位,中文占2個字節(jié),16位。
在表中,long最大為=2^63-1,而double為2^1024,
java中int和float都是32位,long和double都是64位。為什么float和double表示的范圍大那么多呢?因為double采用IEEE-745浮點數(shù)表示法
double是n*2^m(n乘以2的m次方)這種形式存儲的,只需要記錄n和m兩個數(shù)就行了,m的值影響范圍大,所以表示的范圍比long大。
但是m越大,n的精度就越小,所以double并不能把它所表示的范圍里的所有數(shù)都能精確表示出來,而long就可以。
float浮點數(shù),小數(shù)點后第7位是部分準(zhǔn)確的。例如,1.0000004就是1.00000035通過得到的,其實際保存和1.0000003相同。1.0000006也是通過舍入得到的。再往前第6位及以后均可以通過小數(shù)準(zhǔn)確表示出來。通常說float數(shù)據(jù)的有效位是6~7位,也是這個原因。一般來說,無論是整數(shù)或者小數(shù),用float表示時,從左邊第一個非0的數(shù)字算起,從高到低的7位是準(zhǔn)確的。此后的數(shù)位是不能保證精確的。
double雙精度浮點數(shù)小數(shù)部分有52位,和上面類似,最低6位(2^-52,2^-51,......)表示的規(guī)格化小數(shù)如下所示。從圖中可以看出,雙精度浮點數(shù)能準(zhǔn)確表示到小數(shù)點后第15位,第16位部分準(zhǔn)確。用double表示時,從左邊第一個非0的數(shù)字起,從高到低的16位是準(zhǔn)確的,此后的數(shù)位不一定精確
盡管浮點數(shù)表示的范圍很廣,但由于精度損失的存在,加上冪次的放大作用,一個浮點數(shù)實際上是表示了周圍的一個有理數(shù)區(qū)間。如果將浮點數(shù)繪制到一個數(shù)軸上,直觀上看,靠近0的部分,浮點數(shù)出現(xiàn)較密集。越靠近無窮大,浮點數(shù)分布越稀疏,一個浮點值代表了周圍一片數(shù)據(jù)。如下圖所示。從這個意義上來說,浮點數(shù)不宜直接比較相等,它們是代表了一個數(shù)據(jù)范圍。實際應(yīng)用中,如果要使用浮點數(shù)計算,一定要考慮精度問題。在滿足精度要求的前提下,計算結(jié)果才是有效的。?
在計算精度要求情形下,例如商業(yè)計算等,應(yīng)該避免使用浮點數(shù),嚴(yán)格采取高精度計算。
浮點數(shù)丟失解決方案
我們常用的分?jǐn)?shù)(特別是在金融的計算方面)都是十進(jìn)制分?jǐn)?shù)1/10,1/100等。或許以后電路設(shè)計或許會支持十進(jìn)制數(shù)字類型以避免這些舍入問題。在這之前,你更愿意使用大整數(shù)進(jìn)行重要的金融計算,例如,要使用整數(shù)‘分’而不是使用小數(shù)‘元’進(jìn)行貨比單位的運算
即在運算前我們把參加運算的數(shù)先升級(10的X的次方)到整數(shù),等運算完后再降級(0.1的X的次方)。
在java里面有BigDecimal庫,js里面有big.jsjs-big-decimal.js。當(dāng)然BCD編碼就是為了十進(jìn)制高精度運算量制。
BCD編碼
BCD編碼(一般指8421BCD碼形式)亦稱二進(jìn)碼十進(jìn)數(shù)或二-十進(jìn)制代碼。用4位二進(jìn)制數(shù)來表示1位十進(jìn)制數(shù)中的0~9這10個數(shù)。一般用于高精度計算。比如會計制度經(jīng)常需要對很長的數(shù)字串作準(zhǔn)確的計算。相對于一般的浮點式記數(shù)法,采用BCD碼,既可保存數(shù)值的精確度,又可免去使電腦作浮點運算時所耗費的時間。
為什么采用二進(jìn)制
二進(jìn)制在電路設(shè)計中物理上更易實現(xiàn),因為電子器件大多具有兩種穩(wěn)定狀態(tài),比如晶體管的導(dǎo)通和截止,電壓的高和低,磁性的有和無等。而找到一個具有十個穩(wěn)定狀態(tài)的電子器件是很困難的。
二進(jìn)制規(guī)則簡單,十進(jìn)制有55種求和與求積的運算規(guī)則,二進(jìn)制僅有各有3種,這樣可以簡化運算器等物理器件的設(shè)計。另外,計算機的部件狀態(tài)少,可以增強整個系統(tǒng)的穩(wěn)定性。
與邏輯量相吻合。二進(jìn)制數(shù)0和1正好與邏輯量“真”和“假”相對應(yīng),因此用二進(jìn)制數(shù)表示二值邏輯顯得十分自然。
可靠性高。二進(jìn)制中只使用0和1兩個數(shù)字,傳輸和處理時不易出錯,因而可以保障計算機具有很高的可靠性
我覺得主要還是因為第一條。如果比如能夠設(shè)計出十進(jìn)制的元器件,那么對于設(shè)計其運算器也不再話下。
JS數(shù)字精度丟失的一些典型問題
兩個簡單的浮點數(shù)相加
0.1 + 0.2 != 0.3 // true
toFixed 不會四舍五入(Chrome)
1.335.toFixed(2) // 1.33
再問問一個問題 :在js數(shù)字類型中浮點數(shù)的最高精度多少位小數(shù)?(16位 or 17位?……why?
JavaScript 能表示并進(jìn)行精確算術(shù)運算的整數(shù)范圍為:[-2^53-1,2^53-1],即從最小值 -9007199254740991 到最大值 9007199254740991 之間的范圍。'9007199254740991'.length//16?
IEEE754 規(guī)定,有效數(shù)字第一位默認(rèn)總是1 。因此,在表示精度的位數(shù)前面,還存在一個 “隱藏位” ,固定為 1 ,但它不保存在 64 位浮點數(shù)之中。也就是說,有效數(shù)字總是 1.xx...xx 的形式,其中 xx..xx 的部分保存在 64 位浮點數(shù)之中,最長為52位 。所以,JavaScript 提供的有效數(shù)字最長為 53 個二進(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