咱們日常使用的編程語言大多都有一個問題——浮點型精度運算會不許確。好比java
double num = 0.1 + 0.1 + 0.1;
// 輸出結果爲 0.30000000000000004
double num2 = 0.65 - 0.6;
// 輸出結果爲 0.05000000000000004
複製代碼
筆者在測試的時候發現 C/C++ 居然不會出現這種問題,我最初覺得是編譯器優化,把這個問題解決了。可是 C/C++ 若是能解決其餘語言爲何不跟進?根據這個問題的產生緣由來看,編譯器優化解決這個問題邏輯不通。後來發現是打印的方法有問題,打印輸出方法會四捨五入。使用
printf("%0.17f\n", num);
以及cout << setprecision(17) << num2 << endl;
多打印幾位小數便可看到精度運算不許確的問題。python
那麼精度運算不許確這是爲何呢?咱們接下來就須要從計算機全部數據的表現形式二進制提及了。若是你們很瞭解二進制與十進制的相互轉換,那麼就能輕易的知道精度運算不許確的問題緣由是什麼了。若是不知道就讓咱們一塊兒回顧一下十進制與二進制的相互轉換流程。通常狀況下二進制轉爲十進制咱們所使用的是按權相加法
。十進制轉二進制是除2取餘,逆序排列法
。很熟的同窗能夠略過。git
// 二進制到十進制
10010 = 0 * 2^0 + 1 * 2^1 + 0 * 2^2 + 0 * 2^3 + 1 * 2^4 = 18
// 十進制到二進制
18 / 2 = 9 .... 0
9 / 2 = 4 .... 1
4 / 2 = 2 .... 0
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1
10010
複製代碼
那麼,問題來了十進制小數和二進制小數是如何相互轉換的呢?十進制小數到二進制小數通常是整數部分除 2 取餘,逆序排列
,小數部分使用乘 2 取整數位,順序排列
。二進制小數到十進制小數仍是使用按權相加法
。github
// 二進制到十進制
10.01 = 1 * 2^-2 + 0 * 2^-1 + 0 * 2^0 + 1 * 2^1 = 2.25
// 十進制到二進制
// 整數部分
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1
// 小數部分
0.25 * 2 = 0.5 .... 0
0.5 * 2 = 1 .... 0
// 結果 10.01
複製代碼
轉小數咱們也瞭解了,接下來咱們迴歸正題,爲何浮點運算會有精度不許確的問題。接下來咱們看一個簡單的例子 2.1 這個十進制數轉成二進制是什麼樣子的。編程
2.1 分紅兩部分
// 整數部分
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1
// 小數部分
0.1 * 2 = 0.2 .... 0
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
............
複製代碼
落入無限循環結果爲 10.0001100110011........ , 咱們的計算機在存儲小數時確定是有長度限制的,因此會進行截取部分小數進行存儲,從而致使計算機存儲的數值只能是個大概的值,而不是精確的值。從這裏看出來咱們的計算機根本就沒法使用二進制來精確的表示 2.1 這個十進制數字的值,連表示都沒法精確表示出來,計算確定是會出現問題的。bash
現有有三種辦法數據結構
通常每種語言都用高精度運算的解決方法(比通常運算耗費性能),好比 Python 的 decimal 模塊,Java 的 BigDecimal,可是必定要把小數轉成字符串傳入構造,否則仍是有坑,其餘語言你們能夠自行尋找一下。編程語言
# Python 示例
from decimal import Decimal
num = Decimal('0.1') + Decimal('0.1') + Decimal('0.1')
print(num)
複製代碼
// Java 示例
import java.math.BigDecimal;
BigDecimal add = new BigDecimal("0.1").add(new BigDecimal("0.1")).add(new BigDecimal("0.1"));
System.out.println(add);
複製代碼
上面既然提到了浮點型的存儲是有限制,那麼咱們看一下咱們的計算機是如何存儲浮點型的,是否是真的正如咱們上面提到的有小數長度的限制。 那咱們就以 Float 的數據存儲結構來講,根據 IEEE 標準浮點型分爲符號位,指數位和尾數位三部分(各部分大小詳情見下圖)。性能
通常狀況下咱們表示一個很大或很小的數一般使用科學記數法,例如: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 存儲這樣的數據。
咱們舉個小例子。查看一個單精度浮點型 1.25 的具體存儲結構。可使用如下 Python 代碼來查看存儲 float 的具體數據結構。
import struct
num = 1.1
bins = ''.join('{:0>8b}'.format(c) for c in struct.pack('!f', num))
print(bins)
複製代碼
Java 版本的代碼有點長,我就放個連接了。 Java 版代碼連接
經過上述程序咱們獲得的存儲 1.25 的 float 二進制結構的具體值爲 00111111101000000000000000000000 ,咱們拆分一下 0 爲符號位他是個正值。01111111 爲指數位,01000000000000000000000 是尾數。接下來咱們驗證一下 01111111 轉爲十進制是 127,那麼通過計算指數爲 0。尾數是 01000000000000000000000 加上默認省略的 1 爲 1.01(省略後面多餘的 0),轉換爲十進制小數就是 1.25。