解決float型數據精度損失問題

問題:浮點型數據存儲方式會致使數據精度損失,增大計算偏差。html

float fval = 0.45;  // 單步調試發現其真實值爲:0.449999988spa

double dval = 0.45; // 單步調試發現其真實值爲:0.450000000000000013d

當不少個這樣的單精度浮點型數據進行運算時,就會有累積偏差,使得運算結果達不到理想的結果。尤爲是對那種須要判斷相等的狀況(浮點型數據判斷相等會有偏差)。調試

所以咱們能夠經過把浮點型數據放大1e6倍,把它賦給一個整型變量,把獲得的結果再除以1e6,就會使精度損失降到最低。code

float a = 0.45;    // 0.449999988
double b = 0.45;    
int c = 1e6 * b;    // 450000
double d1 = 2 * a;   // 0.89999997615814209
int d = 2 * c;     // 900000
double d2 = d / 1e6;  // 0.90000000000000002, 推薦方式
double d3 = 2 * 0.45; // 0.90000000000000002

下面是浮點型數據存儲方式,轉自:https://www.cnblogs.com/wuyuan2011woaini/p/4105765.htmlhtm

C語言和 C#語言中,對於浮點型的數據採用單精度類型(float)和雙精度類型(double)來存儲:blog

float 數據佔用 32bit;內存

double 數據佔用 64bit;string

咱們在聲明一個變量 float f = 2.25f 的時候,是如何分配內存的呢?it

其實不管是 float 類型仍是 double 類型,在存儲方式上都是聽從IEEE的規範:

float 聽從的是 IEEE R32.24;

double 聽從的是 IEEE R64.53;

 

單精度雙精度在存儲中,都分爲三個部分:

符號位 (Sign):0表明正數,1表明爲負數;

指數位 (Exponent):用於存儲科學計數法中的指數數據;

尾數部分 (Mantissa):採用移位存儲尾數部分;

 

單精度 float 的存儲方式以下:

雙精度 double 的存儲方式以下:

R32.24 和 R64.53 的存儲方式都是用科學計數法來存儲數據的,好比:

8.25  用十進制表示爲:8.25 * 100

120.5 用十進制表示爲:1.205 * 102

 

而計算機根本不認識十進制的數據,他只認識0和1。因此在計算機存儲中,首先要將上面的數更改成二進制的科學計數法表示:

8.25   用二進制表示爲:1000.01

118.5 用二進制表示爲:1110110.1

 

而用二進制的科學計數法表示 1000.1,能夠表示爲1.0001 * 23

而用二進制的科學計數法表示 1110110.1,能夠表示爲1.1101101 * 26

 

任何一個數的科學計數法表示都爲1. xxx * 2,尾數部分就能夠表示爲xxxx,因爲第一位都是1嘛,幹嗎還要表示呀?因此將小數點前面的1省略。

由此,23bit的尾數部分,能夠表示的精度卻變成了24bit,道理就是在這裏。(float有效位數相應的也會發生變化,而double則不會,因達不到

 

 

那 24bit 能精確到小數點後幾位呢?咱們知道9的二進制表示爲1001,因此 4bit 能精確十進制中的1位小數點,24bit就能使 float 精確到小數點後6位;

而對於指數部分,由於指數可正可負(佔1位),因此8位的指數位能表示的指數範圍就只能用7位,範圍是:-127至128。因此指數部分的存儲採用移位存儲,存儲的數據爲元數據 +127。

注意:

元數據+127:大概是指「指數」從00000000開始(表示-127)至11111111(表示+128)

因此,10000000表示指數1 (127 + 1 = 128 --> 10000000 ) ;

指數爲 3,則爲 127 + 3 = 130,表示爲 01111111 + 11 = 10000010 ;

 

下面就看看 8.25 和 118.5 在內存中真正的存儲方式:

8.25 用二進制表示爲:1000.01

8.25 用二進制的科學計數法表示爲: 1.0001* 2,按照上面的存儲方式:

    符號位爲:0,表示爲正;

    指數位爲:3+127=130,即 10000011;

    尾數部分爲:0001;

故8.25的存儲方式以下圖所示:

 

而單精度浮點數118.5的存儲方式以下圖所示:

那麼若是給出內存中一段數據,而且告訴你是單精度存儲的話,你將如何知道該數據的十進制數值呢?

其實就是對上面運算的反推過程,好比給出以下內存數據:01000010111011010000000000000000,

首先咱們現將該數據分段:0  10000101  11011010000000000000000,在內存中的存儲就爲下圖所示:

根據咱們的計算方式,能夠計算出這樣一組數據表示爲:

1.1101101*2(133-127=6) = 1.1101101 * 2= 1110110.1=118.5

 

而雙精度浮點數的存儲和單精度的存儲大同小異,不一樣的是指數部分和尾數部分的位數。因此這裏再也不詳細的介紹雙精度的存儲方式了,只將118.5的最後存儲方式圖給出:

下面就這個知識點來解決一個疑惑,請看下面一段程序,注意觀察輸出結果:

複製代碼
class 浮點數
    {
        static void Main(string[] args)
        {
            float f = 2.2f;
            double d = (double)f;
            Console.WriteLine(d.ToString("0.0000000000000"));
            //結果:"2.2000000476837"

            f = 2.25f;
            d = (double)f;
            Console.WriteLine(d.ToString("0.0000000000000"));
            //結果:"2.2500000000000"

            //2.25 - 2.2 = 0.05 ( 但實際結果不是0.05 )
            float f2 = 2.25f - 2.2f;
            Console.WriteLine(f2.ToString("0.0000000000000"));
            //結果:"0.0499999500000"
        }
    }
複製代碼

輸出的結果可能讓你們迷惑不解:

單精度的 2.2 轉換爲雙精度後,精確到小數點後13位以後變爲了2.2000000476837

而單精度的 2.25 轉換爲雙精度後,變爲了2.2500000000000

 

爲什麼 2.2 在轉換後的數值更改了,而 2.25 卻沒有更改呢?

其實經過上面關於兩種存儲結果的介紹,咱們大概就能找到答案。

2.25 的單精度存儲方式表示爲:0 10000001 00100000000000000000000

2.25 的雙精度存儲方式表示爲:0 10000000 0010010000000000000000000000000000000000000000000000000

這樣 2.25 在進行強制轉換的時候,數值是不會變的。

 

 

而咱們再看看 2.2用科學計數法表示應該爲:

將十進制的小數轉換爲二進制的小數的方法是:將小數*2,取整數部分。

0.2×2=0.4,因此二進制小數第一位爲0.4的整數部分0;

0.4×2=0.8,第二位爲0.8的整數部分0;

0.8×2=1.6,第三位爲1;

0.6×2=1.2,第四位爲1;

0.2×2=0.4,第五位爲0;

...... 這樣永遠也不可能乘到=1.0,獲得的二進制是一個無限循環的排列 00110011001100110011...

 

對於單精度數據來講,尾數只能表示 24bit 的精度,因此2.2的 float 存儲爲:

可是這種存儲方式,換算成十進制的值,卻不會是2.2。

 

由於在十進制轉換爲二進制的時候可能會不許確(如:2.2),這樣就致使了偏差問題!

而且 double 類型的數據也存在一樣的問題!

因此在浮點數表示中,均可能會不可避免的產生些許偏差!

在單精度轉換爲雙精度的時候,也會存在一樣的偏差問題。

 

而對於有些數據(如2.25),在將十進制轉換爲二進制表示的時候剛好可以計算完畢,因此這個偏差就不會存在,也就出現了上面比較奇怪的輸出結果。

相關文章
相關標籤/搜索