浮點數精度丟失問題

2021-06-27 18:54:29 字數 4309 閱讀 3953

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 ∈ ;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)。

同樣的,對於 double,11位指數字,儲存時採用的偏移值為 1023。

尾數字,由於所有數值均可以轉換成 1.*** * (2 ^ n)(此處暫時忽略精度問題),所以尾數部分只儲存小數部分(最高位的 1 不存入記憶體,提高 1 個位的精度)。

以 float 198903.19 為例,二進位制為 1.100001000111101110011000010100011 * (2 ^ 17);

數值為正數,符號位是 0;

指數是 17,儲存為 144(17 + 127 = 144),即 10010000(共 8 位,不足 8 位在高位用 0 補足);

小數字是 10000100011110111001100(擷取 23 位);

最終得到:0

1001000 01000010 00111101 11001100,按位元組倒序順序,轉為十六進製制就是:cc 3d 42 48

同樣的格式,double 198903.19 最終得到:0

1000001 00001000 01000111 10111001 10000101 00011110 10111000 01010010(最後兩位結果為什麼是 10 而不是 01,請參考

浮點數的捨入

),按位元組倒序順序,轉為十六進製制就是:52 b8 1e 85 b9 47 08 41

回到精度丟失的問題,由於小數字無法算盡,記憶體用擷取精度的方式儲存了轉換後的二進位制,這導致儲存的結果並非是完全正確的數值。

看回 程式段1 的例子,

num_a 在記憶體中其實是儲存為:0

1001000 01000010 00111101 11001100,換算成十進位制就是:198903.1875;

num_b 在記憶體中其實是儲存為:0

1000111 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 這兩個值,那麼就只有可能是在輸出的時候做了變動。其實這是微軟做的小把戲,我們有句俗話說"以毒攻毒",大概就這個意思,既然儲存的已經是不正確的數值,那麼在輸出的時候,會智慧型地猜測判斷原先正確的數值是什麼,然後輸出猜測的那個值,說不定就真的猜中了呢!

如果有寫錯的地方,請幫忙指正。謝謝 ...

JS之浮點數精度丟失問題

js中,在進行浮點數計算的時候,會出現精度丟失的問題,如下 alert 0.1 0.2 alert 0.3 0.2 alert 0.1 0.2 alert 0.3 0.2 正常情況,得出的結果應該是0.3,0.1,0.02,1.5,但是實際得出的結果卻不是這樣,如下 可以看到,得出的結果並不是我們預...

浮點數的運算精度丟失

開啟python編譯器,輸入0.1 0.2,期待的結果是0.3,但是輸出為 0.30000000000000004 有點小尷尬,這是為什麼呢?其實這設計到了計算機的浮點數儲存是以二進位制進行儲存的。說二進位制不太形象,換成我們最長使用的十進位制和分數 1 5,使用小數表示為0.2,但是1 3,使用小...

浮點數的運算精度丟失

開啟python編譯器,輸入0.1 0.2,期待的結果是0.3,但是輸出為 0.30000000000000004 有點小尷尬,這是為什麼呢?其實這設計到了計算機的浮點數儲存是以二進位制進行儲存的。說二進位制不太形象,換成我們最長使用的十進位制和分數 1 5,使用小數表示為0.2,但是1 3,使用小...