爲何浮點精度運算會有問題

爲何浮點精度運算會有問題

咱們日常使用的編程語言大多都有一個問題——浮點型精度運算會不許確。好比java

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 標準浮點型分爲符號位,指數位和尾數位三部分(各部分大小詳情見下圖)。spa

IEEE 754 標準code

通常狀況下咱們表示一個很大或很小的數一般使用科學記數法,例如:1000.00001 咱們通常表示爲 1.00000001 10^3,或者 0.0001001 通常表示爲 1.001 10^-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。

參考:《2020最新Java基礎精講視頻教程和學習路線!》

相關文章
相關標籤/搜索