浮點類型計算精度不准確原因及如何規避


一、精度丟失的原因

首先我們要搞清楚下面兩個問題:
(1) 十進制整數如何轉化為二進制數
           算法很簡單。舉個例子,11表示成二進制數:
                     11/2=5 余   1
                     5/2=2   余   1
                     2/2=1   余   0
                     1/2=0   余   1
                     0結束         11二進制表示為(從下往上):1011

這里提一點:只要遇到除以后的結果為0了就結束了,大家想一想,所有的整數除以2是不是一定能夠最終得到0。換句話說,所有的整數轉變為二進制數的算法會不會無限循環下去呢?絕對不會,整數永遠可以用二進制精確表示 ,但小數就不一定了。

(2) 十進制小數如何轉化為二進制數
算法是乘以2直到沒有了小數為止。舉個例子,0.9表示成二進制數
   0.9*2=1.8   取整數部分 1
   0.8(1.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   取整數部分 0
    .........      
   0.9二進制表示為(從上往下): 1100100100100......
注意:上面的計算過程循環了,也就是說*2永遠不可能消滅小數部分,這樣算法將無限下去。很顯然,小數的二進制表示有時是不可能精確的 。其實道理很簡單,十進制系統中能不能准確表示出1/3呢?同樣二進制系統也無法准確表示1/10。這也就解釋了為什么浮點型減法出現了"減不盡"的精度丟失問題。

  

二、float存儲原理

眾所周知、 Java 的float型在內存中占4個字節。float的32個二進制位結構如下

float內存存儲結構

             4bytes      31    30    29----23    22----0         

             表示       實數符號位    指數符號位        指數位          有效數位

        其中符號位1表示正,0表示負。有效位數位24位,其中一位是實數符號位。

         將一個float型轉化為內存存儲格式的步驟為:

        (1)先將這個實數的絕對值化為二進制格式,注意實數的整數部分和小數部分的二進制方法在上面已經探討過了。 
     (2)將這個二進制格式實數的小數點左移或右移n位,直到小數點移動到第一個有效數字的右邊。 
     (3)從小數點右邊第一位開始數出二十三位數字放入第22到第0位。 
     (4)如果實數是正的,則在第31位放入“0”,否則放入“1”。 
     (5)如果n 是左移得到的,說明指數是正的,第30位放入“1”。如果n是右移得到的或n=0,則第30位放入“0”。 
     (6)如果n是左移得到的,則將n減去1后化為二進制,並在左邊加“0”補足七位,放入第29到第23位。如果n是右移得到的或n=0,則將n化為二進制后在左邊加“0”補足七位,再各位求反,再放入第29到第23位。

          舉例說明: 11.9的內存存儲格式

       (1) 將11.9化為二進制后大約是" 1011. 1110011001100110011001100..."。

       (2) 將小數點左移三位到第一個有效位右側: "1. 011 11100110011001100110 "。 保證有效位數24位,右側多余的截取(誤差在這里產生了 )。

       (3) 這已經有了二十四位有效數字,將最左邊一位“1”去掉,得到“ 011 11100110011001100110 ”共23bit。將它放入float存儲結構的第22到第0位。

       (4) 因為11.9是正數,因此在第31位實數符號位放入“0”。

       (5) 由於我們把小數點左移,因此在第30位指數符號位放入“1”。

       (6) 因為我們是把小數點左移3位,因此將3減去1得2,化為二進制,並補足7位得到0000010,放入第29到第23位。

           最后表示11.9為: 0 1 0000010 011 11100110011001100110

           再舉一個例子:0.2356的內存存儲格式
      (1)將0.2356化為二進制后大約是0.00111100010100000100100000。 
      (2)將小數點右移三位得到1.11100010100000100100000。 
      (3)從小數點右邊數出二十三位有效數字,即11100010100000100100000放
入第22到第0位。 
      (4)由於0.2356是正的,所以在第31位放入“0”。 
      (5)由於我們把小數點右移了,所以在第30位放入“0”。 
      (6)因為小數點被右移了3位,所以將3化為二進制,在左邊補“0”補足七
位,得到0000011,各位取反,得到1111100,放入第29到第23位。 


           最后表示0.2356為:0 0 1111100 11100010100000100100000

          將一個內存存儲的float二進制格式轉化為十進制的步驟: 
     (1)將第22位到第0位的二進制數寫出來,在最左邊補一位“1”,得到二十四位有效數字。將小數點點在最左邊那個“1”的右邊。 
     (2)取出第29到第23位所表示的值n。當30位是“0”時將n各位求反。當30位是“1”時將n增1。 
     (3)將小數點左移n位(當30位是“0”時)或右移n位(當30位是“1”時),得到一個二進制表示的實數。 
     (4)將這個二進制實數化為十進制,並根據第31位是“0”還是“1”加上正號或負號即可。

  

三、浮點類型減法運算

浮點加減運算過程比定點運算過程復雜。完成浮點加減運算的操作過程大體分為四步:

(1) 0操作數的檢查;
        如果判斷兩個需要加減的浮點數有一個為0,即可得知運算結果而沒有必要再進行有序的一些列操作。 

(2) 比較階碼(指數位)大小並完成對階;
    兩浮點數進行加減,首先要看兩數的 指數位 是否相同,即小數點位置是否對齊。若兩數 指數位 相同,表示小數點是對齊的,就可以進行尾數的加減運算。反之,若兩數階碼不同,表示小數點位置沒有對齊,此時必須使兩數的階碼相同,這個過程叫做對階 。

    如何對 階(假設兩浮點數的指數位為 Ex 和 Ey ):
    通過尾數的移位以改變 Ex 或 Ey ,使之相等。 由 於浮點表示的數多是規格化的,尾數左移會引起最高有位的丟失,造成很大誤差;而尾數右移雖引起最低有效位的丟失,但造成的誤差較小,因此,對階操作規定使 尾數右移,尾數右移后使階碼作相應增加,
其數值保持不變。很顯然,一個增加后的階碼與另一個相等,所增加的階碼一定是小階。因此在對階時,總是使小階向大階看齊 ,即小階的尾數向右移位 ( 相當於小數點左移 ) ,每右移一位,其階碼加 1 ,直到兩數的階碼相等為止,右移的位數等於階差 △ E 。 (3) 尾數(有效數位)進行加或減運算; (4) 結果規格化並進行舍入處理。

  

四、浮點類型標識的有效數字及數值范圍

1、Float:比特數為32,有效數字為6-7,數值范圍為 -3.4E+38 和 3.4E+38

2、Double:比特數為64,有效數字為15-16,數值范圍為-1.7E-308~1.7E+308

對於單精度浮點數(float)來說,有一位符號位,指數位共8位,尾數共23位。指數能夠表示的指數范圍為-128~127。尾數為23位。當尾數全1時再加上小數點前面的1,指數取到最大正數127(8位,正數最大127,負數最小-128),浮點數取得正數的最大值。
+1.111111111111111111111*2^127(1.后面23個1,由於尾數的范圍1~2,其最高位總為1,故只需存取小數部分,所以小數為是23位1),約等於2*2^127=3.4*10^38。為3.4*10^38負數亦然。

Double的計算與此類似,double的符號位為63位,指數為62~52位,共11位。表示的范圍為-1024~1023。尾數為51~0。表示的范圍為+1.111111111111111111111*2^1023(1.后面52個1)為1.7*10^308。負數亦然。

 

五、BigDecimal替代

大多數情況下,使用double和float計算的結果是准確的,但是在一些精度要求很高的系統中或者已知的小數計算得到的結果會不准確,這種問題是非常嚴重的。

《Effective Java》中提到一個原則,那就是float和double只能用來作科學計算或者是工程計算,但在商業計算中我們要用java.math.BigDecimal,通過使用BigDecimal類可以解決上述問題,java的設計者給編程人員提供了一個很有用的類BigDecimal,
他可以完善float和double類無法進行精確計算的缺憾。 使用BigDecimal,但一定要用BigDecimal(String)構造器,而千萬不要用BigDecimal(double)來構造(也不能將float或double型轉換成String再來使用BigDecimal(String)來構造,因為在將float或double轉換成String時精度已丟失)。例如new BigDecimal(0.1),
它將返回一個BigDecimal,也即0.1000000000000000055511151231257827021181583404541015625,正確使用BigDecimal,程序就可以打印出我們所期望的結果0.9: Java代碼 System.out.println(new BigDecimal("2.0").subtract(new BigDecimal("1.10")));// 0.9 另外,如果要比較兩個浮點數的大小,要使用BigDecimal的compareTo方法。

 

六、BigDecimal如何解決精度問題

BigDecimal的底層數據結構使什么?它是怎么保證精度的?

這曾經是一道阿里巴巴的面試題,由於在工作中使用最多的是BigDecimal的加、減、乘、除的的方法,還真沒想過它的實現原理(完全是拿來主義惹的禍),乍這么一問還真有點懵。BigDecimal保證精度的解決思路其實極其的簡單朴素,
還是用一句話來解釋:十進制整數在轉化成二進制數時不會有精度問題,那么把十進制小數擴大N倍讓它在整數的維度上進行計算,並保留相應的精度信息。 ———————————————— 源碼中 scale字段記錄精度信息;intCompact字段記錄放大的整數信息

 

 

 

參考:

https://www.cnblogs.com/zouhao/p/6713230.html

https://zhidao.baidu.com/question/10538690.html

https://blog.csdn.net/jipeng19/article/details/85811099

 


免責聲明!

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



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