程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 浮點數精度丟失問題

浮點數精度丟失問題

編輯:C#入門知識

C#中的浮點數,分單精度(float)和雙精度(double):   float 是 System.Single 的別名,介於 -3.402823e38 和 +3.402823e38 之間的32位數字,符合二進制浮點算法的 IEC 60559:1989 (IEEE 754) 標准;   double 是 System.Double 的別名,介於 -1.79769313486232e308 和 +1.79769313486232e308 之間的64位數字,符合二進制浮點算法的 IEC 60559:1989 (IEEE 754) 標准;       我們知道,計算機只認識 0 和 1,所以數值都是以二進制的方式儲存在內存中的。   (對於人腦和計算機哪個聰明,個人更傾向於選擇人腦,計算機只是計算得快,而且不厭其煩而已!)   所以要知道數值在內存中是如何儲存的,需先將數值轉為二進制(這裡指在范圍內的數值)。   根據 IEEE 754 標准,任意一個二進制浮點數 V 均可表示為:V = (-1 ^ s) * M * (2 ^ e)。   其中 s ∈ {0, 1};M ∈ [1, 2);e 表示偏移指數。   以 198903.19(10) 為例,先轉成二進制的數值為:110000100011110111.0011000010100011(2)(截取 16 位小數),采用科學記數法等於 1.100001000111101110011000010100011 * (2 ^ 17)(整數位是 1),即 198903.19(10) = (-1 ^ 0) * 1.100001000111101110011000010100011 * (2 ^ 17)。   整數部分可采用 "除2取余法",小數部分可采用 "乘2取整法"。   從結果可以看出,小數部分 0.19 轉為二進制後,小數位數超過 16 位(我已經手算到小數點後 32 位都還沒算完,其實這個位數是無窮盡的)。   由於無法得到完全正確的數值,這裡就引申出浮點數精度丟失的問題:   /* 程序段1 */ float num_a = 198903.19f; float num_b = num_a / 2; Console.WriteLine(num_a); Console.WriteLine(num_b); 這段程序代碼,我們預想中正確的結果應該是:198903.19 和 99451.595。   但結果居然是!!!原因下面將講到 ...       這裡介紹另一種轉小數部分的方法,有興趣可以看下:   假如結果要求精確到 N 位小數,那麼只需要將小數部分乘以 2 的 N 次方(例如 N = 16,0.19 * (2 ^ 16),得到 12451.84)。   取整數部分(12451),按整數的方法轉為二進制,得到 11000010100011,不足 N 位在高位用 0 補足。   結果 0.19 精確到 16 位後,用二進制表示為 0.0011000010100011。   可以看出,若是小數部分乘以 2 的 N 次方後,可以得到一個整數,那麼這個小數可以用二進制精確表示,否則則不可以。   (原理很簡單,根據二進制小數位轉十進制的方法,反推回去就可以得到這個結果)       在內存中,float 和 double 的儲存格式是一致的,只是占用的空間大小不同。   float 總共占用 32 位:       從左往右,第 1 位是符號位,占 1 位;第 2-9 位是指數位,占 8 位;第 10-32 位是尾數位,占 23 位。   double 總共占用 64 位,從左往右第 1 位也是符號位,占 1 位;第 2-12 位是指數位,占 11 位;第 13-64 位是尾數位,占 52 位。       其中,符號位(即上文的 s,下同),0 代表正數,1 代表負數。   對於 float,8位指數位的值范圍為 0-255(10),由於指數(即上文的 e,下同)可正可負,而指數位的值是一個無符號整數。根據標准規定,儲存時采用偏移值(偏移值為127)的方法,儲存值為指數 + 127。例如 0111 0011(2) 表示指數 -12(10)((-12) + 127 = 115),1000 1011(2) 表示指數 12(10)(12+ 127 = 139)。   {     另外,IEEE 754 規定(同樣適用於 double):     當指數全為 0 時,如果尾數全為 0,表示 ±0(正負取決於符號位),如果尾數不全為 0,計算時指數等於 -126,尾數不加上第一位的1,而是還原為 0.xxxxxx 的小數,表示更接近 0 的小數;     當指數全為 1 時,如果尾數全為 0,表示 ±無窮大(正負取決於符號位),如果尾數不全為 0,表示這不是一個數(NaN)。     資料來自非規約形式的浮點數   }   同樣的,對於 double,11位指數位,儲存時采用的偏移值為 1023。   尾數位,由於所有數值均可以轉換成 1.xxx * (2 ^ N)(此處暫時忽略精度問題),所以尾數部分只保存小數部分(最高位的 1 不存入內存,提高 1 個位的精度)。       以 float 198903.19 為例,二進制為 1.100001000111101110011000010100011 * (2 ^ 17);   數值為正數,符號位是 0;   指數是 17,保存為 144(17 + 127 = 144),即 10010000(共 8 位,不足 8 位在高位用 0 補足);   小數位是 10000100011110111001100(截取 23 位);   最終得到:01001000 01000010 00111101 11001100,按字節倒序順序,轉為十六進制就是:CC 3D 42 48   float f_num = 198903.19f; var f_bytes = BitConverter.GetBytes(f_num); Console.WriteLine("float: 198903.19"); Console.WriteLine(BitConverter.ToString(f_bytes)); Console.WriteLine(string.Join(" ", f_bytes.Select(i => Convert.ToString(i, 2).PadLeft(8, '0'))));         同樣的格式,double 198903.19 最終得到:01000001 00001000 01000111 10111001 10000101 00011110 10111000 01010010(最後兩位結果為什麼是 10 而不是 01,請參考浮點數的捨入),按字節倒序順序,轉為十六進制就是:52 B8 1E 85 B9 47 08 41   double d_num = 198903.19d; var d_bytes = BitConverter.GetBytes(d_num); Console.WriteLine("double: 198903.19"); Console.WriteLine(BitConverter.ToString(d_bytes)); Console.WriteLine(string.Join(" ", d_bytes.Select(i => Convert.ToString(i, 2).PadLeft(8, '0'))));         回到精度丟失的問題,由於小數位無法算盡,內存用截取精度的方式儲存了轉換後的二進制,這導致保存的結果並非是完全正確的數值。   看回 程序段1 的例子,   num_a 在內存中其實是保存為:01001000 01000010 00111101 11001100,換算成十進制就是:198903.1875;   num_b 在內存中其實是保存為:01000111 11000010 00111101 11001100,換算成十進制就是:99451.59375;   先看 num_b,由於 num_a 在內存中儲存的值已經是不正確的,那麼再利用其進行計算,得到的結果 99.9% 也會是不正確的。所以 num_b 的結果並不是我們想要的 99451.595。   然後為什麼 198903.1875 會變成 198903.2,而 99451.59375 會變成 99451.595 呢?我們知道,內存中確實是儲存了 198903.1875 和 99451.59375 這兩個值,那麼就只有可能是在輸出的時候做了變動。其實這是微軟做的小把戲,我們有句俗話說"以毒攻毒",大概就這個意思,既然儲存的已經是不正確的數值,那麼在輸出的時候,會智能地猜測判斷原先正確的數值是什麼,然後輸出猜測的那個值,說不定就真的猜中了呢!   (之前看過的一篇文章寫的,忘了地址,大概就這個意思。如果不是因為這個原因,大家就當飯後娛樂吧。)    

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved