Go:浮點數精度丟失問題詳解

請看以下 Go 代碼,會返回 0.7 嗎?

    var num float32
    for i := 0; i < 7; i++{
        num = num + 0.1
    }
    fmt.Println(num)
    
0.70000005

還有,除了語言之外,你還可以在 MySQL 等數據庫中試試 float 類型數據的字段疊加,得到的數據是否精確。

要了解產生這個現象的原因,就要先了解計算機是如何定義和表示 float 類型的。不同於正整數類型的表示方法,float 類型在計算機中的表示略顯複雜,遵循的是 IEEE754標準

下面,我們就講一下 IEEE754標準

我們首先回顧一下整數類型在計算機中的表示。我們知道: 計算機只認識 0 和 1;那麼,對於像 6 一樣的這種正整數,我們要做十進制到二進制的轉換。即:

所以,十進制 6最終轉化爲二進制爲 110

這很好理解,但是,如何表示 6.1等這類小數呢?有人說了,可以找個特殊的符號,用來表示小數點 .,把 6.161隔開;聽起來是個不錯的辦法。其實 IEEE754還真就是這麼做的,只不過思路略有些複雜,總體思路就是:仿照用 "科學計數法"!

我們再回顧一下什麼是 科學計數法把一個數表示成a與10的n次冪相乘的形式(1≤|a|<10,a不爲分數形式,n爲整數),這種記數法叫做科學記數法。也就是:1.360X10^4 這種計數方式。

我們可以仿照科學計數法,來表示浮點數,把二進制數統一表示成 1.0110101X2^n這種形式。數據層面怎麼表示出這種形式呢?根據 IEEE754的標準,將數據分爲三部分:

從左到右分別表示:符號位 (正負數)、指數位和小數位

以單精度浮點數爲例,單精度浮點數一共 32 位 (雙精度 64 位,即平時所說的 double類型),具體內部表示爲:

這裏有個地方要特別注意:因爲數據最終要表示成 1.0110101X2^n這種形式,整數位在二進制下,永遠都是 1,所以在表示 float 類型的時候,直接把 1給去掉了,假如有就佔據一個 bit 的空間,既然那個 bit 位上永遠都是 1,所以乾脆去掉了。

那麼,具體該如何展示呢?例如小數點後的數字怎麼表示?6.1能否寫成 110.1呢?如果能的話小數點後這個 1 代表什麼呢?個數一?那添加幾個零的話,能否認爲是十、一百、一千?似乎是不可以,因爲這樣只能滿足 "視覺效果", 邏輯層面直接說不通。

要明白在小數點後的數字代表除以 2 後的數字,例如二進制下小數點後的第一位 1 代表 1 / 2 等於 0.5,第二位 1 代表 1/2/2 等於 0.25,依次類推第三位 1 則代表 0.125... 具體請看下圖:

所以,給定一個小數,譬如 0.1,要想得到對應的二進制數,應該是和小數點左邊的計算方式相反:乘以2,記錄整數位

    0.1 X 2 = 0.2  0
    0.2 X 2 = 0.4  0
    0.4 X 2 = 0.8  0
    0.8 X 2 = 1.6  1
    (1.6 - 1 = 0.6)
    0.6 X 2 = 1.2  1
    (1.2 - 1 = 0.2) 
    0.2 X 2 = 0.4  0
    0.4 X 2 = 0.8  0
    0.8 X 2 = 1.6  1
    (1.6 - 1 = 0.6)
    0.6 X 2 = 1.2  1
    (1.2 - 1 = 0.2) 
    0.2 X 2 = 0.4  0
    0.4 X 2 = 0.8  0
    0.8 X 2 = 1.6  1
    ... 
    // 無限循環下去

所以, 0.1 用二進制表示爲:0.000110011001100110011...因此 6.1 用二進制應該表示爲:110.000110011001100110011...用” 科學計數法 “表示爲:1.10000110011001100110011...X2^2OK,看來小數位的數可以確定了是 10000110011001100110011,即去掉整數位 1 後,向後截取的 23 位數 (浮點數不精確的本質原因)。

符號位 0 表示正數,1 表示負數,所以可以確定是 6.1的符號位是 0;現在符號位有了,小數位有了,只剩下指數 2 如的表示了,該如何表示呢?直接在 8 位的空間內轉化爲 000000010

顯然不可以,首先,如果指數位用 原碼表示,那麼,針對指數位爲負的情況,就得加一個符號位去表示,而且還會出現兩個零的情況:000000001000000,操作起來過程複雜~

有人要問那如果使用補碼呢?如果使用補碼,會出現以下情況,請看例子:

可見使用補碼,也不是很方便,於是,引用了另外一種編碼方式——- 移碼。先說說移碼的定義:將每一個數值加上一個偏置常數(Excess/bias),通常,當編碼位數爲n的時候,bias取"2^n-1"或者"2^n-1 - 1"

承接以上 1.01 X 2^-1 和 1.11 X 2^3 比較大小的例子:

1.01 X 2^-1 和 1.11 X 2^3大小?
指數:
-1 + 4 = 3,二進制表示爲:"011"
3 + 4 = 7 二進制表示爲:"111"
7 > 3,即 "111" > "011" 比較完畢

就這樣,浮點數”科學計數法 “的指數位比較變得簡單了,而且,消除了” 正零 “ 和 ” 負零“ 不相同的問題。

因爲 :

假設偏移量是:4
則移碼錶示的0只有:0 + 4 = 4,即“100”

IEEE754中,指數位移碼的偏移量爲指數位數的 2^n-1-1,爲 127。

所以,回到 6.1表示的問題上,指數位爲:2+127=129,二進制表示爲:10000001

因此, 6.1IEEE754單精度浮點數標準的下,表示爲:

好了,現在瞭解了浮點數 IEEE754標準的表示方法,知道爲何浮點數相加總是不精確了吧?

因爲浮點數很多小數在二進制環境下很多都無法完整的表示,只能截取部分數據來近似的表示,兩個數相加的話,就是兩個近似的數相加的和,如果相加次數足夠多,精確度自然也就會越來越低

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xw37DwMwkWay8RWfJ3OCLg