1. 程式人生 > 其它 >為什麼浮點精度運算會有問題

為什麼浮點精度運算會有問題

我們平常使用的程式語言大多都有一個問題——浮點型精度運算會不準確,今天通過本篇文章跟大家詳細聊聊此事。

為什麼浮點精度運算會有問題

我們平常使用的程式語言大多都有一個問題——浮點型精度運算會不準確。比如

1.  double num = 0.1 + 0.1 + 0.1;
    
2.  // 輸出結果為 0.30000000000000004
    
3.  double num2 = 0.65 - 0.6;
    
4.  // 輸出結果為 0.05000000000000004
筆者在測試的時候發現 C/C++ 竟然不會出現這種問題,我最初以為是編譯器優化,把這個問題解決了。但是 C/C++ 如果能解決其他語言為什麼不跟進?根據這個問題的產生原因來看,編譯器優化解決這個問題邏輯不通。後來發現是列印的方法有問題,列印輸出方法會四捨五入。使用printf("%0.17fn", num);
以及cout << setprecision(17) << num2 << endl;多列印幾位小數即可看到精度運算不準確的問題。

那麼精度運算不準確這是為什麼呢?我們接下來就需要從計算機所有資料的表現形式二進位制說起了。如果大家很瞭解二進位制與十進位制的相互轉換,那麼就能輕易的知道精度運算不準確的問題原因是什麼了。如果不知道就讓我們一起回顧一下十進位制與二進位制的相互轉換流程。一般情況下二進位制轉為十進位制我們所使用的是按權相加法。十進位制轉二進位制是除2取餘,逆序排列法。很熟的同學可以略過。

1.  // 二進位制到十進位制
    
2.  10010 = 0 * 2^0 + 1 * 2^1 + 0 * 2^2 + 0 * 2^3 + 1 * 2^4 = 18  
    

4.  //
十進位制到二進位制 5. 18 / 2 = 9 .... 0 6. 9 / 2 = 4 .... 1 7. 4 / 2 = 2 .... 0 8. 2 / 2 = 1 .... 0 9. 1 / 2 = 0 .... 1 11. 10010

那麼,問題來了十進位制小數和二進位制小數是如何相互轉換的呢?十進位制小數到二進位制小數一般是整數部分除 2 取餘,逆序排列小數部分使用乘 2 取整數位,順序排列。二進位制小數到十進位制小數還是使用按權相加法

1.  // 二進位制到十進位制
    
2.  10.01 = 1 * 2^-2 + 0 * 2^-1 + 0 * 2^0 + 1 * 2^1 = 2.25
    

4.  //
十進位制到二進位制 5. // 整數部分 6. 2 / 2 = 1 .... 0 7. 1 / 2 = 0 .... 1 8. // 小數部分 9. 0.25 * 2 = 0.5 .... 0 10. 0.5 * 2 = 1 .... 1 12. // 結果 10.01

轉小數我們也瞭解了,接下來我們迴歸正題,為什麼浮點運算會有精度不準確的問題。接下來我們看一個簡單的例子 2.1 這個十進位制數轉成二進位制是什麼樣子的。

1.  2.1 分成兩部分
    
2.  // 整數部分
    
3.  2 / 2 = 1 .... 0
    
4.  1 / 2 = 0 .... 1
    

6.  // 小數部分
    
7.  0.1 * 2 = 0.2 .... 0
    
8.  0.2 * 2 = 0.4 .... 0
    
9.  0.4 * 2 = 0.8 .... 0
    
10.  0.8 * 2 = 1.6 .... 1
    
11.  0.6 * 2 = 1.2 .... 1
    
12.  0.2 * 2 = 0.4 .... 0
    
13.  0.4 * 2 = 0.8 .... 0
    
14.  0.8 * 2 = 1.6 .... 1
    
15.  0.6 * 2 = 1.2 .... 1
    
16.  0.2 * 2 = 0.4 .... 0
    
17.  0.4 * 2 = 0.8 .... 0
    
18.  0.8 * 2 = 1.6 .... 1
    
19.  0.6 * 2 = 1.2 .... 1
    
20.  ............

落入無限迴圈結果為 10.0001100110011........ , 我們的計算機在儲存小數時肯定是有長度限制的,所以會進行擷取部分小數進行儲存,從而導致計算機儲存的數值只能是個大概的值,而不是精確的值。從這裡看出來我們的計算機根本就無法使用二進位制來精確的表示 2.1 這個十進位制數字的值,連表示都無法精確表示出來,計算肯定是會出現問題的。

精度運算丟失的解決辦法

現有有三種辦法

  1. 如果業務不是必須非常精確的要求可以採取四捨五入的方法來忽略這個問題。
  2. 轉成整型再進行計算。
  3. 使用 BCD 碼儲存和運算二進位制小數(感興趣的同學可自行搜尋學習)。

一般每種語言都用高精度運算的解決方法(比一般運算耗費效能),比如 Python 的 decimal 模組,Java 的 BigDecimal,但是一定要把小數轉成字串傳入構造,不然還是有坑,其他語言大家可以自行尋找一下。

1.  # Python 示例
    
2.  from decimal import Decimal
    

4.  num = Decimal('0.1') + Decimal('0.1') + Decimal('0.1')
    
5.  print(num)
    



1.  // Java 示例
    
2.  import java.math.BigDecimal;
    

4.  BigDecimal add = new BigDecimal("0.1").add(new BigDecimal("0.1")).add(new BigDecimal("0.1"));
    
5.  System.out.println(add);

拓展:詳解浮點型

上面既然提到了浮點型的儲存是有限制,那麼我們看一下我們的計算機是如何儲存浮點型的,是不是真的正如我們上面提到的有小數長度的限制。
那我們就以 Float 的資料儲存結構來說,根據 IEEE 標準浮點型分為符號位,指數位和尾數位三部分(各部分大小詳情見下圖)。

IEEE 754 標準

一般情況下我們表示一個很大或很小的數通常使用科學記數法,例如:1000.00001 我們一般表示為 1.0000000110^3,或者 0.0001001 一般表示為 1.00110^-4。

符號位

0 是正數,1 是負數

指數位

指數很有意思因為它需要表示正負,所以人們創造了一個叫 EXCESS 的系統。這個系統是什麼意思呢?它規定 最大值 / 2 - 1 表示指數為 0。我們使用單精度浮點型舉個例子,單精度浮點型指數位一共有八位,表示的十進位制數最大就是 255。那麼 255 / 2 - 1 = 127,127 就代表指數為 0。如果指數位儲存的十進位制資料為 128 那麼指數就是 128 - 127 = 1,如果儲存的為 126,那麼指數就是 126 - 127 = -1。

尾數位

比如上述例子中 1.00000001 以及 1.001 就屬於尾數,但是為什麼叫尾數呢?因為在二進位制中比如 1.xx 這個小數,小數點前面的 1 是永遠存在的,存了也是浪費空間不如多存一位小數,所以尾數位只會儲存小數部分。也就是上述例子中的 00000001 以及 001 儲存這樣的資料。

IEEE 754 標準

通過上述程式我們得到的儲存 1.25 的 float 二進位制結構的具體值為 00111111101000000000000000000000 ,我們拆分一下 0 為符號位他是個正值。01111111 為指數位,01000000000000000000000 是尾數。接下來我們驗證一下 01111111 轉為十進位制是 127,那麼經過計算指數為 0。尾數是 01000000000000000000000 加上預設省略的 1 為 1.01(省略後面多餘的 0),轉換為十進位制小數就是 1.25。