1. 程式人生 > >float型別的精度問題與計算機中的儲存

float型別的精度問題與計算機中的儲存

定點數表達法的缺點在於其形式過於僵硬,固定的小數點位置決定了固定位數的整數部分和小數部分,不利於同時表達特別大的數或者特別小的數。
計算機系統採納了所謂的浮點數表達方式。這種表達方式利用科學計數法來表達實數,即用一個尾數(Mantissa也叫有效數字 ),一個基數(Base),一個指數(Exponent)以及

一個表示正負的符號來表達實數。浮點數利用指數達到了浮動小數點的效果,從而可以靈活地表達更大範圍的實數。


當一個浮點數的尾數為0,不論其階碼為何值,該浮點數的值都為0。當階碼的值為它能表示的最小一個值或更小的值時,不管其尾數為何值,計算機都把該浮點數看成零值,通常稱

其為機器零,此時該浮點數的所有各位(包括階碼位和尾數位)都清為0值。

Java 平臺上的浮點數型別 float 和 double 採納了 IEEE 754 標準中所定義的單精度 32 位浮點數和雙精度 64 位浮點數的格式。

在 IEEE 標準中,浮點數是將特定長度的連續位元組的所有二進位制位分割為特定寬度的符號域,指數域和尾數域三個域,其中儲存的值分別用於表示給定二進位制浮點數中的符號,指

數和尾數。這樣,通過尾數和可以調節的指數就可以表達給定的數值了。具體的格式參見下面的圖例:




在上面的圖例中,第一個域為符號域。其中 0 表示數值為正數,而 1 則表示負數。

第二個域為指數域,對應於我們之前介紹的二進位制科學計數法中的指數部分。其中單精度數為 8 位,雙精度數為 11 位。以單精度數為例,8 位的指數為可以表達 0 到 255 之間

的 255 個指數值(注:指數8位的最高位都是數值位,沒有符號位)。但是,指數可以為正數,也可以為負數。為了處理負指數的情況,實際的指數值按要求需要加上一個偏差

(Bias)值作為儲存在指數域中的值,單精度數的偏差值為 127(2^7-1),而雙精度數的偏差值為1023(2^10-1)。比如,單精度的實際指數值 0 在指數域中將儲存為 127;而

儲存在指數域中的 64 則表示實際的指數值 -63(64-127=-63)。 偏差的引入使得對於單精度數,實際可以表達的指數值的範圍就變成 -127(表示小數點需向左移動127位) 到

128(表示小數點需向右移動128位) 之間(包含兩端)。我們不久還將看到,實際的指數值 -127(全 0)以及 +128(全 1)保留用作特殊值的處理。這樣,實際可以表達的有效

指數範圍就在 -127 和 127 之間。

第三個域為尾數域,其中單精度數為 23 位長,雙精度數為 52 位長。除了我們將要講到的某些特殊值外,IEEE 標準要求浮點數必須是規範的。這意味著尾數的小數點左側必須為

1,因此我們在儲存尾數的時候,可以省略小數點前面這個 1,從而騰出一個二進位制位來儲存更多的尾數。這樣我們實際上用 23 位長的尾數域表達了 24 位的尾數。比如對於單精

度數而言,二進位制的 1001.101(對應於十進位制的 9.625)可以表達為 1.001101 × 2^3,所以實際儲存在尾數域中的值為 00110100000000000000000,即去掉小數點左側的 1,並

用 0 在右側補齊。
值得注意的是,對於單精度數,由於我們只有 24 位的尾數(其中一位隱藏),所以可以表達的最大尾數為 2^24- 1 = 16,777,215。特別的,16,777,216 是偶數,所以我們可以

通過將它除以 2 並相應地調整指數來儲存這個數,這樣 16,777,216 同樣可以被精確的儲存。相反,數值 16,777,217 則無法被精確的儲存。由此,我們可以看到單精度的浮點數

可以表達的十進位制數值中,真正有效的數字不高於 8 位。事實上,對相對誤差的數值分析結果顯示有效的精度大約為 7.22 位(由於位數不可取小數,所以單精度的精度為7,即

可精確到小數點後7位)。參考下面的示例:
Java程式碼

   1. System.out.println(16777215f);//1.6777215E7 
   2. System.out.println(16777216f);//1.6777216E7 
   3. System.out.println(16777217f);//1.6777216E7 
   4. System.out.println(16777218f);//1.6777218E7 
   5. System.out.println(16777219f);//1.677722E7 
   6. System.out.println(16777220f);//1.677722E7 
   7. System.out.println(16777221f);//1.677722E7 
   8. System.out.println(16777222f);//1.6777222E7 
   9. System.out.println(16777223f);//1.6777224E7 
  10. System.out.println(16777224f);//1.6777224E7 
  11. System.out.println(16777225f);//1.6777224E7 

  System.out.println(16777215f);//1.6777215E7
  System.out.println(16777216f);//1.6777216E7
  System.out.println(16777217f);//1.6777216E7
  System.out.println(16777218f);//1.6777218E7
  System.out.println(16777219f);//1.677722E7
  System.out.println(16777220f);//1.677722E7
  System.out.println(16777221f);//1.677722E7
  System.out.println(16777222f);//1.6777222E7
  System.out.println(16777223f);//1.6777224E7
  System.out.println(16777224f);//1.6777224E7
  System.out.println(16777225f);//1.6777224E7

請看結果推導分析:

111111111111111111111111          16777215f
1.11111111111111111111111          剛好是23位,不會丟失精度,能精確表示
0 23+127 11111111111111111111111
0 10010110 11111111111111111111111



1000000000000000000000000         16777216f
1.00000000000000000000000 0        去掉的是0,所以還是能準確表示
0 24+127 00000000000000000000000
0 10010111 00000000000000000000000



1000000000000000000000001          16777217f
1.00000000000000000000000 1        不能準確表示。先試著進位
1.00000000000000000000001          由於進位後,結果的最末們不是0,所以直接舍掉
1.00000000000000000000000          到這裡結果就是16777216f



1000000000000000000000010         16777218f
1.0000000000000000000001 0         去掉的是0,所以還是能準確表示
0 24+127 00000000000000000000001
0 10010111 00000000000000000000001



1000000000000000000000011         16777219f
1.0000000000000000000001 1         不能準確表示。先試著進位
1.0000000000000000000010           進位後的結果,最末們為0,所以進位成功
0 24+127 00000000000000000000010
0 10010111 00000000000000000000010
1.000000000000000000000100*2^24    16777219f儲存在記憶體中的結果實質上為16777220

........



根據標準要求,無法精確儲存的值必須向最接近的可儲存的值進行舍入。這有點像我們熟悉的十進位制的四捨五入,0就舍,但1不一定就進,而是在前後兩個等距接近的可儲存的值

中,取其中最後一位有效數字為零者(即先試著進1,會得到最後結果,然後看這個結果的尾數最後位是否為0,如果是則進位,否則直接捨去)。從上面的示例中可以看出,奇數

都被舍入為偶數(舍入到偶數有助於從某些角度減小計算中產生的舍入誤差累積問題。因此為 IEEE 標準所採用),且有舍有進。我們可以將這種舍入誤差理解為"半位"的誤差。

所以,為了避免 7.22 對很多人造成的困惑,有些文章經常以 7.5來說明單精度浮點數的精度問題。


假定我們有一個 32 位的資料,用十六進位制表示為 0xC0B40000,並且我們知道它實際上是一個單精度的浮點數。為了得到該浮點數實際表達的實數,我們首先將它變換為二進位制形

式:
1100 0000 1011 0100 0000 0000 0000 0000
接著按照浮點數的格式切分為相應的域:
1 10000001 01101000000000000000000
符號域 1 意味著負數;指數域為 129 意味著實際的指數為 2 (減去偏差值 127);尾數域為 01101 意味著實際的二進位制尾數為 1.01101 (加上隱含的小數點前面的 1)。所以

,實際的實數為:
-1.01101 × 2^2 = -101.101 = -5.625
或使用Java也可計算出:
Java程式碼

   1. /*
   2. 注:Float.intBitsToFloat 方法是將記憶體中int數值的二進位制看作是float的
   3. 二進位制制,這樣很方便使用一個二進位制位來構造一個float。
   4. */ 
   5. System.out.println(Float.intBitsToFloat(0xc0B40000));//-5.625 

/*
注:Float.intBitsToFloat方法是將記憶體中int數值的二進位制看作是float的
二進位制制,這樣很方便使用一個二進位制位來構造一個float。
*/
System.out.println(Float.intBitsToFloat(0xc0B40000));//-5.625



從實數向浮點數變換稍微麻煩一點。假定我們需要將實數 -9.625 表達為單精度的浮點數格式。方法是首先將它用二進位制浮點數表達,然後變換為相應的浮點數格式。
首先,將小數點左側的整數部分變換為其二進位制形式,9 的二進位制性形式為 1001。處理小數部分的演算法是將我們的小數部分乘以基數 2,記錄乘積結果的整數部分,接著將結果的

小數部分繼續乘以 2,並不斷繼續該過程:
0.625 × 2 = 1.25   1
0.25× 2 = 0.5        0
0.5× 2 = 1            1
                            0
當最後的結果為零時,結束這個過程。這時右側的一列數字就是我們所需的二進位制小數部分,即 0.101。這樣,我們就得到了完整的二進位制形式 1001.101。用規範浮點數表達為

1.001101 × 2^3。
因為是負數,所以符號域為 1。指數為 3,所以指數域為 3 + 127 = 130,即二進位制的 10000010。尾數省略掉小數點左側的 1 之後為 001101,右側用零補齊。最終結果為:
1 10000010 00110100000000000000000
最後可以將浮點數形式表示為十六進位制的資料如下:
1100 0001 0001 1010 0000 0000 0000 0000
或使用Java也可計算出:
Java程式碼

   1. /*
   2. 注:Float.floatToIntBits:將記憶體中的float型數值的二進位制看作是對應
   3. 的int型別的二進位制,這樣很方便的將一個float的記憶體資料以二進位制來表示。
   4. 11000001000110100000000000000000
   5. */ 
   6. System.out.println(Integer.toBinaryString(Float.floatToIntBits(-9.625F))); 

/*
注:Float.floatToIntBits:將記憶體中的float型數值的二進位制看作是對應
的int型別的二進位制,這樣很方便的將一個float的記憶體資料以二進位制來表示。
11000001000110100000000000000000
*/
System.out.println(Integer.toBinaryString(Float.floatToIntBits(-9.625F)));



很簡單?等等!你可能已經注意到了,在上面這個我們有意選擇的示例中,不斷的將產生的小數部分乘以 2 的過程掩蓋了一個事實。該過程結束的標誌是小數部分乘以 2 的結果

為 1,不難想象,很多小數根本不能經過有限次這樣的過程而得到結果(比如最簡單的 0.1)。我們已經知道浮點數尾數域的位數是有限的,為此,浮點數的處理辦法是持續該過

程直到由此得到的尾數足以填滿尾數域,之後對多餘的位進行舍入。換句話說,除了我們之前講到的精度問題之外,十進位制到二進位制的變換也並不能保證總是精確的,而只能是近

似值。事實上,只有很少一部分十進位制小數具有精確的二進位制浮點數表達。再加上浮點數運算過程中的誤差累積,結果是很多我們看來非常簡單的十進位制運算在計算機上卻往往出

人意料。這就是最常見的浮點運算的"不準確"問題。參見下面的 Java 示例:
Java程式碼

   1. // 34.6-34.0=0.5999985 
   2. System.out.print("34.6-34.0=" + (34.6f-34.0f)); 

// 34.6-34.0=0.5999985
System.out.print("34.6-34.0=" + (34.6f-34.0f));

產生這個誤差的原因是 34.6 無法精確的表達為相應的浮點數,而只能儲存為經過舍入的近似值。這個近似值與 34.0 之間的運算自然無法產生精確的結果。



當指數、尾數都為0,則規定該浮點數為0。
當指數255(全1),且尾數為0時,表示無窮大,用符號位來確定是正無窮大還是負無窮大。
當指數255(全1),且尾數不為0時,表示NaN(不是一個數)。
最大的float數:0,11111110,1111111 11111111 11111111   用10進製表示約為   +3.4E38
最小的float數:1,11111110,1111111 11111111 11111111   用10進製表示約為   -3.4E38
絕對值最小的float 數:0,00000000,0000000 00000000 00000001和1,00000000,0000000   00000000 00000001


浮點數的精度:
單精度數的尾數用23位儲存,加上預設的小數點前的1位1,2^(23+1) = 16777216。因為 10^7 < 16777216 < 10^8,所以說單精度浮點數的有效位數(精度)是7位(即小數點後7

位)。
雙精度的尾數用52位儲存,2^(52+1) = 9007199254740992,10^16 < 9007199254740992 < 10^17,所以雙精度的有效位數(精度)是16位(即小數點後16位)。
如果你在浮點數的有效位後增加數字的話,結果是不會變化的:
Java程式碼

   1. System.out.println(1.67772156F);//1.6777215 
   2. System.out.println(1.67772150123456789D);//1.6777215012345679 

System.out.println(1.67772156F);//1.6777215
System.out.println(1.67772150123456789D);//1.6777215012345679



float取值範圍:
負數取值範圍為 -3.4028235E+38 ~ -1.401298E-45,正數取值範圍為 1.401298E-45 ~ 3.4028235E+38。
Float.MIN_VALUE:保持 float 型別資料的最小正非零值的常量,最小正非零值為2^-149。該常量等於十六進位制的浮點文字 0x0.000002P-126f,也等於 Float.intBitsToFloat

(0x1)=1.4e-45f。(二進位制為:0,00000000,0000000 00000000 00000001)
Float.MAX_VALUE:保持 float 型別的最大正有限大值的常量,最大正有限大值為(2-2^-23)*2^127。該常量等於十六進位制的浮點文字 0x1.fffffeP+127f,也等於

Float.intBitsToFloat(0x7f7fffff)=3.4028235e+38f。(二進位制為:0,11111110,1111111 11111111 11111111)
Java程式碼

   1. public class FloatTest { 
   2.     public static void main(String[] args) { 
   3.         // 只要指數為255,而尾數不為0時,該數不是一個數 
   4.         System.out.println(format(Integer.toBinaryString(0x7FC00000)) + " " 
   5.                 + Float.intBitsToFloat(0x7FC00000));// NaN 
   6.         System.out.println(format(Integer.toBinaryString(0xFFC00000)) + " " 
   7.                 + Float.intBitsToFloat(0xFFC00000));// NaN 
   8.         System.out.println(format(Integer.toBinaryString(0x7F800001)) + " " 
   9.                 + Float.intBitsToFloat(0x7F800001));// NaN 
  10.         // 指數為255,尾數為0時,該數就是無窮,符號位區分正無窮與負無窮 
  11.         System.out.println(format(Integer.toBinaryString(0x7F800000)) + " " 
  12.                 + Float.intBitsToFloat(0x7F800000));// Infinity 
  13.         System.out.println(format(Integer.toBinaryString(0xFF800000)) + " " 
  14.                 + Float.intBitsToFloat(0xFF800000));// Infinity 
  15.         //規定指數與尾數都為0時,規定結果就是0 
  16.         System.out.println(format(Integer.toBinaryString(0x0)) + " " 
  17.                 + Float.intBitsToFloat(0x0));        
  18.         // 正的最小float,趨近於0 
  19.         System.out.println(format(Integer.toBinaryString(0x1)) + " " 
  20.                 + Float.intBitsToFloat(0x1));// 1.4E-45 
  21.         // 正的最大float 
  22.         System.out.println(format(Integer.toBinaryString(0x7f7fffff)) + " " 
  23.                 + Float.intBitsToFloat(0x7f7fffff));// 3.4028235E38 
  24.         // 負的最大float,趨近於0 
  25.         System.out.println(format(Integer.toBinaryString(0x80000001)) + " " 
  26.                 + Float.intBitsToFloat(0x80000001));// -1.4E-45 
  27.         // 負的最小float 
  28.         System.out.println(format(Integer.toBinaryString(0xFf7fffff)) + " " 
  29.                 + Float.intBitsToFloat(0xFf7fffff));// -3.4028235E38 
  30.     } 
  31.  
  32.     private static String format(String str) { 
  33.         StringBuffer sb = new StringBuffer(str); 
  34.         int sub = 32 - str.length(); 
  35.         if (sub != 0) { 
  36.             for (int i = 0; i < sub; i++) { 
  37.                 sb.insert(0, 0); 
  38.             } 
  39.         } 
  40.         sb.insert(1, " "); 
  41.         sb.insert(10, " "); 
  42.         return sb.toString(); 
  43.     } 
  44. } 

public class FloatTest {
public static void main(String[] args) {
// 只要指數為255,而尾數不為0時,該數不是一個數
System.out.println(format(Integer.toBinaryString(0x7FC00000)) + " "
+ Float.intBitsToFloat(0x7FC00000));// NaN
System.out.println(format(Integer.toBinaryString(0xFFC00000)) + " "
+ Float.intBitsToFloat(0xFFC00000));// NaN
System.out.println(format(Integer.toBinaryString(0x7F800001)) + " "
+ Float.intBitsToFloat(0x7F800001));// NaN
// 指數為255,尾數為0時,該數就是無窮,符號位區分正無窮與負無窮
System.out.println(format(Integer.toBinaryString(0x7F800000)) + " "
+ Float.intBitsToFloat(0x7F800000));// Infinity
System.out.println(format(Integer.toBinaryString(0xFF800000)) + " "
+ Float.intBitsToFloat(0xFF800000));// Infinity
//規定指數與尾數都為0時,規定結果就是0
System.out.println(format(Integer.toBinaryString(0x0)) + " "
+ Float.intBitsToFloat(0x0));
// 正的最小float,趨近於0
System.out.println(format(Integer.toBinaryString(0x1)) + " "
+ Float.intBitsToFloat(0x1));// 1.4E-45
// 正的最大float
System.out.println(format(Integer.toBinaryString(0x7f7fffff)) + " "
+ Float.intBitsToFloat(0x7f7fffff));// 3.4028235E38
// 負的最大float,趨近於0
System.out.println(format(Integer.toBinaryString(0x80000001)) + " "
+ Float.intBitsToFloat(0x80000001));// -1.4E-45
// 負的最小float
System.out.println(format(Integer.toBinaryString(0xFf7fffff)) + " "
+ Float.intBitsToFloat(0xFf7fffff));// -3.4028235E38
}

private static String format(String str) {
StringBuffer sb = new StringBuffer(str);
int sub = 32 - str.length();
if (sub != 0) {
for (int i = 0; i < sub; i++) {
sb.insert(0, 0);
}
}
sb.insert(1, " ");
sb.insert(10, " ");
return sb.toString();
}
}


2^-149=(0.00000000000000000000001) *2^-127=(00000000000000000000000.1) *2^-149=0x0.000002P-126f=0.0000 0000 0000 0000 0000 0010*2^126
(2-2^-23)*2^127=(10.00000000000000000000000-0.00000000000000000000001) *2^127
0x1.fffffeP+127f=0x1.1111 1111 1111 1111 1111 1110P+127



double取值範圍:
負值取值範圍 -1.79769313486231570E+308 ~ -4.94065645841246544E-324,正值取值範圍為 4.94065645841246544E-324 ~ 1.79769313486231570E+308。
Double.MIN_VALUE:保持 double 型別資料的最小正非零值的常量,最小正非零值為 2^-1074。它等於十六進位制的浮點字面值 0x0.0000000000001P-1022,也等於

Double.longBitsToDouble(0x1L)。
Double.MAX_VALUE 保持 double 型別的最大正有限值的常量,最大正有限值為 (2-2^-52)*2^1023。它等於十六進位制的浮點字面值 0x1.fffffffffffffP+1023,也等於

Double.longBitsToDouble(0x7fefffffffffffffL)。



Java中的小數精確計算
Java程式碼

   1. public class FloatCalc { 
   2.  
   3.     //預設除法運算精度    
   4.     private static final int DEF_DIV_SCALE = 10; 
   5.  
   6.     /**   
   7.     *   提供精確的加法運算。   
   8.     *   @param   v1   被加數   
   9.     *   @param   v2   加數   
  10.     *   @return   兩個引數的和   
  11.     */ 
  12.  
  13.     public static double add(double v1, double v2) { 
  14.         BigDecimal b1 = new BigDecimal(Double.toString(v1)); 
  15.         BigDecimal b2 = new BigDecimal(Double.toString(v2)); 
  16.         return b1.add(b2).doubleValue(); 
  17.     } 
  18.  
  19.     /**   
  20.     *   提供精確的減法運算。   
  21.     *   @param   v1   被減數   
  22.     *   @param   v2   減數   
  23.     *   @return   兩個引數的差   
  24.     */ 
  25.  
  26.     public static double sub(double v1, double v2) { 
  27.         BigDecimal b1 = new BigDecimal(Double.toString(v1)); 
  28.         BigDecimal b2 = new BigDecimal(Double.toString(v2)); 
  29.         return b1.subtract(b2).doubleValue(); 
  30.     } 
  31.  
  32.     /**   
  33.       *   提供精確的乘法運算。   
  34.       *   @param   v1   被乘數   
  35.       *   @param   v2   乘數   
  36.       *   @return   兩個引數的積   
  37.       */ 
  38.  
  39.     public static double mul(double v1, double v2) { 
  40.         BigDecimal b1 = new BigDecimal(Double.toString(v1)); 
  41.         BigDecimal b2 = new BigDecimal(Double.toString(v2)); 
  42.         return b1.multiply(b2).doubleValue(); 
  43.     } 
  44.  
  45.     /**   
  46.       *   提供(相對)精確的除法運算,當發生除不盡的情況時,精確到   
  47.       *   小數點以後10位,以後的數字四捨五入。   
  48.       *   @param   v1   被除數   
  49.       *   @param   v2   除數   
  50.       *   @return   兩個引數的商   
  51.       */ 
  52.  
  53.     public static double div(double v1, double v2) { 
  54.         return div(v1, v2, DEF_DIV_SCALE); 
  55.     } 
  56.  
  57.     /**   
  58.       *   提供(相對)精確的除法運算。當發生除不盡的情況時,由scale引數指   
  59.       *   定精度,以後的數字四捨五入。   
  60.       *   @param   v1   被除數   
  61.       *   @param   v2   除數   
  62.       *   @param   scale   表示表示需要精確到小數點以後幾位。   
  63.       *   @return   兩個引數的商   
  64.       */ 
  65.  
  66.     public static double div(double v1, double v2, int scale) { 
  67.         if (scale < 0) { 
  68.             throw new IllegalArgumentException( 
  69.                     "The scale must be a positive integer or zero"); 
  70.         } 
  71.         BigDecimal b1 = new BigDecimal(Double.toString(v1)); 
  72.         BigDecimal b2 = new BigDecimal(Double.toString(v2)); 
  73.         return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); 
  74.     } 
  75.  
  76.     /**   
  77.       *   提供精確的小數位四捨五入處理。   
  78.       *   @param   v   需要四捨五入的數字   
  79.       *   @param   scale   小數點後保留幾位   
  80.       *   @return   四捨五入後的結果   
  81.       */ 
  82.  
  83.     public static double round(double v, int scale) { 
  84.         if (scale < 0) { 
  85.             throw new IllegalArgumentException( 
  86.                     "The scale must be a positive integer or zero"); 
  87.         } 
  88.         BigDecimal b = new BigDecimal(Double.toString(v)); 
  89.         BigDecimal one = new BigDecimal("1"); 
  90.         return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); 
  91.     } 
  92. }