1. 程式人生 > >float型別精度損失問題

float型別精度損失問題

float範圍為: 32 3.4E–038~3.4E+038
double範圍為: 64 1.7E–308~1.7E+308

#include <stdio.h>
    #define EPSILON 0.0001 
    // Define your own tolerance#define FLOAT_EQ(x,v) (((v - EPSILON) < x) && (x <( v + EPSILON)))
int main() 

    float a, b, c; 
    a = 1.345f; 
    b = 1.123f; 
    c = a + b; 
// if (FLOAT_EQ(c, 2.468))          Remove comment for correct result 

    if (c == 2.468) // Comment this line for correct result
        printf("They are equal.\n");
    else 
        printf("They are not equal! The value of c is %13.10f " "or %f
",c,c);
}
           答案:the value of c is   2.4679999352 or 2.468000 .....



                  float,double等精度丟失問題 float,double記憶體表示  
 
為何浮點數可能丟失精度浮點十進位制值通常沒有完全相同的二進位制表示形式。 這是 CPU 所採用的浮點資料表示形式的副作用。 為此,可能會經歷一些精度丟失,並且一些浮點運算可能會產生意外的結果。

導致此行為的原因是下面之一:

十進位制數的二進位制表示形式可能不精確

使用的數字之間型別不匹配(例如,混合使用浮點型和雙精度型)

為解決此行為,大多數程式設計師或是確保值比需要的大或者小,或是獲取並使用可以維護精度的二進位制編碼的十進位制 (BCD) 庫。

現在我們就詳細剖析一下浮點型運算為什麼會造成精度丟失?

1、小數的二進位制表示問題

首先我們要搞清楚下面兩個問題:

(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。這也就解釋了為什麼浮點型減法出現了"減不盡"的精度丟失問題。

2、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. 01111100110011001100110"。保證有效位數24位(不含小數點),右側多餘的擷取(誤差在這裡產生了)。

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

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

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

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

最後表示11.9為: 01000001001111100110011001100110

再舉一個例子: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為:001111100 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”加上正號或負號即可。

3、浮點型的減法運算

浮點加減運算過程比定點運算過程複雜。完成浮點加減運算的操作過程大體分為四步:
  (1) 0運算元的檢查;

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

   (2) 比較階碼(指數位)大小並完成對階;

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

如何對階(假設兩浮點數的指數位為Ex和Ey):

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

對階完畢後就可有效數位求和。不論是加法運算還是減法運算,都按加法進行操作,其方法與定點加減運算完全一樣。
   (4) 結果規格化並進行舍入處理。

4、計算12.0f-11.9f

12.0f 的記憶體儲存格式為: 01000001010000000000000000000000

11.9f 的記憶體儲存格式為: 01000001001111100110011001100110


可見兩數的指數位完全相同,只要對有效數位進行減法即可。

12.0f-11.9f 結果: 01000001000000011001100110011010

將結果還原為十進位制為: 0.00011001100110011010=0.10000038

詳細的分析

由於對float或double 的使用不當,可能會出現精度丟失的問題。問題大概情況可以通過如下程式碼理解:

view plaincopy to clipboardprint?
public class FloatDoubleTest { 
public static void main(String[] args) { 
float f = 20014999; 
double d = f; 
double d2 = 20014999; 
System.out.println("f=" + f); 
System.out.println("d=" + d); 
System.out.println("d2=" + d2); 


public class FloatDoubleTest {
public static void main(String[] args) {
float f = 20014999;
double d = f;
double d2 = 20014999;
System.out.println("f=" + f);
System.out.println("d=" + d);
System.out.println("d2=" + d2);
}
}

得到的結果如下:

f=2.0015E7

d=2.0015E7

d2=2.0014999E7

從輸出結果可以看出double 可以正確的表示20014999 ,而float 沒有辦法表示20014999 ,得到的只是一個近似值。這樣的結果很讓人訝異。20014999 這麼小的數字在float下沒辦法表示。於是帶著這個問題,做了一次關於float和double學習,做個簡單分享,希望有助於大家對java 浮點數的理解。

定義數字後面加f的問題:
float x=3.14f  (d表示double)
如果在程式碼中使用一個小數而沒有加任何字尾,則此數字自動視為double型別
double變為float是丟失精度的,因此它被編譯器認定為帶有危險性的行為,一般會彈出一個Warning