浮點數精度問題原因以及各種現象的解釋

在知乎上看到有人提出類似的問題,我總結表述一下,做一下筆記:

一步步解釋:

存儲問題

首先要了解,從根本上講,計算機只識別二進制數,無論我們使用何種編程語言,在何種編譯環境下工作,都需要將程序編譯成二進制機器碼才能讓計算機執行
那么這樣會導致什么問題呢?會導致小數的十進制轉為二進制出現誤差,這就是所謂的精度缺失。

為什么會出現誤差?這就涉及到了計算機原理的問題:

我們知道數字在計算機中是以二進制存儲的。對于整數來說,只要不超過整數的表示范圍,一定都可以表示成二進制的形式,比如8是1000,88是1011000,可是小數小數部分就沒有那么幸運了,根據二進制小數轉換成十進制的規則(把該小數不斷乘2,再取所得的整數部份,直至沒有小數或足夠長度為止)(二進制整數換成十進制規則 除二取余倒記發)。

由于二進制中所有的小數存儲的位數是有限,因此我們得知“任何十進制整數都可以精確轉換成一個二進制整數,但任何十進制小數卻不一定能精確轉換成一個二進制小數,只要轉換過程中乘積的小數部分滿足所需精度即可”。比如對于0.1來說就不能精確轉換為一個二進制小數,在16位小數的限制條件下,離它最近的二進制小數是0.0001100110011,也就是十進制的0.0999755859375。所以雖然你寫程序的時候寫的是0.1開始這個數存儲到b這個float變量里的時候就變成了0.0001100110011,也就是0.0999755859375,因此你輸出它的時候就會出現精度誤差了,這種誤差是不可避免的。
這里我有個疑問,為什么我們不把小數也當做整數存儲呢?我懷疑是因為存儲方式的問題,然后我找到了原碼、反碼、補碼的文章,然后我又想到為什么要用補碼?然后找到了加法器,然后就想為什么只有加法器?到這感覺有點不清楚了,我要好好找一下,有情況再回來更新。

再來,根據IEEE754(二進制浮點數算術標準) 來說:

V = (-1)^s × 2^E × M
  (1)(-1)^s表示符號位,當s=0,V為正數;當s=1,V為負數。

(2)2^E表示指數位。

(3)M表示有效數字,大于等于1,小于2。

IEEE 754規定,有四種精度的浮點數:單精確度(32位)、雙精確度(64位)、延伸單精確度(43比特以上,很少使用)與延伸雙精確度(79比特以上,通常以80位實現)。
對于32位的浮點數,最高的1位是符號位s,接著的8位是指數E,剩下的23位為有效數字M。

32位浮點數存儲格式

對于64位的浮點數,最高的1位是符號位S,接著的11位是指數E,剩下的52位為有效數字M。

64位浮點數存儲格式

其實看看上圖你就應該明白,還有一種精度丟失就是長度丟失(不同類型存儲結構不同),但那種比較簡單,就不做討論。

繼續來講,上圖到底是什么意思呢。咱們先慢慢來講:

正規化(規約形式)

如果浮點數中指數部分的編碼值在[圖片上傳失敗...(image-c806de-1554705679571)]之間,且在科學表示法的表示方式下,分數 (fraction) 部分最高有效位(即整數字)是1,那么這個浮點數將被稱為規約形式的浮點數。
如上存儲格式中所示:

  1. 首先對于第一段sign,就是符號位,0代表正,1代表負。。

  2. 第二段exponent指數位,實際也是有正負的,但是沒有單獨的符號位,在計算機的世界里,進位都是二進制的,指數表示的也是2的N次冪,8位指數表達的范圍是0到255,而對應的實際的指數是-127到128。也就是說實際的指數等于指數位表示的數值減127。這里特殊說明,-127和+128這兩個指數數值在IEEE當中是保留的用作多種用途的,
    關于指數位與實際的指數之差叫做移碼,移碼的值

精度 M(階/指數數位) 移碼 二進制表示
單精度 8 127 0111 1111
雙精度 11 1023 011 1111 1111
長雙精度 15 16383 011 1111 1111 1111
  1. 第三段fraction尾數位(也叫有效數字),只代表了二進制的小數點后的部分,小數點前的那位被省略了,當指數位全部為0時省略的是0否則省略的是1。

舉個栗子(以32位浮點數為例,這樣誤差比較大):

比如有個浮點數17.35需要保存。
17.35轉為二進制:17轉為10001(這就不用說了吧)
0.35呢?(乘2取整法)
0.35 * 2 = 0.7 記為 0
0.70 * 2 = 1.4 記為 1
0.40 * 2 = 0.8 記為 0
0.80 * 2 = 1.6 記為 1
.......這么算有結束嗎?當然沒有...所以就按照能保存的極限位數保存相對精度。  

這樣我們17.35就變成了10001.0101100110011001100共預留24位數  
然后把這串數字右移至小數點前只剩1位:  
1.00010101100110011001100---》 右移了4位  
這樣得到了指數位和尾數位:  
指數:實際為4,必須加上127(轉出的時候,減去127),所以為131。也就是10000011。  
尾數:00010101100100100100100 (1就不用保存了)。  

到此,十進制浮點數就變成了二進制浮點數存儲了。

非正規化:0的表示(非規約形式)(-127)

如果浮點數的指數部分的編碼值是0,分數部分非零,那么這個浮點數將被稱為非規約形式的浮點數。分數部分全為0,就是表示浮點數0

比如說我們要存儲0.35這個浮點數,按上述正規化的形式就無法存儲,因為不知道指數位寫多少,于是就有了約定,約定指數位為0就表明非正規化。
所以,當見到指數部分為0是,尾數部分就不再是1.bbbbb...而是0.bbbbb...了。

無窮大與NAN

上面說了,指數位預留了兩個值(-127和128),-127是做非規約形式的,那么128呢?

當指數位達到當前浮點數最大指數值時:

  • 尾數位全為0就表示無窮大。
  • 尾數位不全為0就表示NaN。

舍入規則

還是以32位單精度浮點數為例。
32位單精度浮點數存儲23位尾數位,那么就以第24位位判斷依據

  • 如果 24位為1 24位之后都是0 :如果23位為0,則舍去不管;如果23位為1,則24位向23位進1,使23位還是0。
  • 如果 24位為1 24位之后不全0 :24位向23位進1。
  • 如果 24位為0 舍

** 因為位數、算法、規則之類的約定,得知計算機浮點數并不是嚴格存儲這就是為什么浮點數運算出現問題的原因 **


有事情要做,先寫到這。
待說明問題:

  1. 關于漸進式下溢出的理論與由來。
  2. 關于以下代碼出現情況的說明:
System.out.println(0.1 * 1);
System.out.println(0.1 * 2);
System.out.println(0.1 * 3);
System.out.println(0.1 * 4);
System.out.println(0.1 * 5);
System.out.println(0.1 * 6);
System.out.println(0.1 * 7);
System.out.println(0.1 * 8);
System.out.println(0.1 * 9);
System.out.println("=====================");
System.out.println(0.0999999999999999999999999999999999999999999999999);
double f1 = 9.7;
double f2 = 0.7;
double f3 = 9;
System.out.println(f1-f2-f3);
System.out.println(f1-f3-f2);

輸出結果:
0.1
0.2
0.30000000000000004
0.4
0.5
0.6000000000000001
0.7000000000000001
0.8
0.9
=====================
0.1
-6.661338147750939E-16

問題:

  1. 為什么有的輸出正確,有的不行
  2. 為什么0.0999--的輸出為0.1
  3. 為什么f1-f2-f3得到0.0,而f1-f3-f2卻得到那樣的結果

這些問題我現在也并沒有思考清楚,等待后續思考。
當然,有問題可以在評論中討論指出。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容