0.1 + 0.2 == 0.3 嗎?

1. 背景

這要從一段 golang 代碼講起:

func main() {
    var a float32 = 0.1
    var b float32 = 0.2
    var c float32 = 0.3
    fmt.Println(a + b == c)     // true
    
    var aa float64 = 0.1
    var bb float64 = 0.2
    var cc float64 = 0.3
    fmt.Println(aa + bb == cc)  // false

	fmt.Println(0.1+0.2 == 0.3) // true
}

爲什麼同樣是 0.1 + 0.2,有時候等於 0.3 有時候又不等呢?本文這就爲您揭祕。

2. 浮點數的計算機表示

浮點數在計算機中一般採用 IEEE754 表示法存儲,相關細節可以在維基百科找到,讀者可以自行閱讀。本文只關注其根本表示形式,即 符號位 + 指數位(階碼) + 尾碼
在這裏插入圖片描述
V 表示小數數值;S 表示符號位;M 表示尾碼;E 表示指數位(階碼)

在內存中的存儲形式如下圖:
在這裏插入圖片描述

3. 十進制小數轉二進制表示

可以使用乘 2 取整法,例如:

2 * 0.1 = 0.2  整數位0
2 * 0.2 = 0.4  整數位0
2 * 0.4 = 0.8  整數位0
2 * 0.8 = 1.6  整數位1
2 * 0.6 = 1.2  整數位1
2 * 0.2 = 0.4  整數位0
2 * 0.4 = 0.8  整數位0
2 * 0.8 = 1.6  整數位1
⋯⋯

那麼,0.1 的二進制表示即爲: 0.00011001100110011…,轉換爲指數表示形式,即爲:1.1001100110011… * 2^(-4)

4. float32 類型的 0.1 + 0.2 運算

float32 類型的小數包含:1 位符號位,8 位階碼,23 位尾碼,使用 “乘 2 取整法” 分別計算 0.1、0.2、0.3 的二進制指數表示形式:

0.1 => 1.10011001100110011001101 * 2^(-4) (小數位第 24 位爲 1,向前進位)
0.2 => 1.10011001100110011001101 * 2^(-3) (小數位第 24 位爲 1,向前進位)
0.3 => 1.00110011001100110011010 * 2^(-2) (小數位第 24 位爲 1,向前連續進位)

計算 0.1 + 0.2,由於階碼不同,需要首先對齊階碼,規則是向大階碼對齊,因此 0.1 的階碼需要由 -4 變爲 -3,即:

0.1 => 0.11001100110011001100111 * 2^(-3)  (原來最右邊的一位被捨棄,但會進位)
0.2 => 1.10011001100110011001101 * 2^(-3)  (対階前後保持不變)

根據上述表示方法,計算二進制加法 0.1 + 0.2 的結果爲:

0.1 + 0.2 = 10.01100110011001100110100 * 2^(-3) = 1.00110011001100110011010 * 2^(-2)
0.3 => 1.00110011001100110011010 * 2^(-2)

由於 0.1 + 0.2 的結果在計算機底層存儲與 0.3 的底層存儲一致,因此 0.1 + 0.2 == 0.3 爲 true

5. float64 類型的 0.1 + 0.2 運算

float64 類型的小數包含:1 位符號位,11 位階碼,52 位尾碼,使用 “乘 2 取整法” 分別計算 0.1、0.2、0.3 的二進制指數表示形式:

0.1 => 1.1001100110011001100110011001100110011001100110011010 * 2^(-4) (小數位 53 位爲 1 ,連續進位)
0.2 => 1.1001100110011001100110011001100110011001100110011010 * 2^(-3) (小數位 53 位爲 1 ,連續進位)
0.3 => 1.0011001100110011001100110011001100110011001100110011 * 2^(-2) (小數位 53 位爲 0 ,捨去)

計算 0.1 + 0.2,由於階碼不同,需要首先對齊階碼,規則是向大階碼對齊,因此 0.1 的階碼需要由 -4 變爲 -3,即:

0.1 => 0.1100110011001100110011001100110011001100110011001101 * 2^(-3)  (原來最右邊的一位被捨棄,但此時不會進位)
0.2 => 1.1001100110011001100110011001100110011001100110011010 * 2^(-3)  (対階前後保持不變)

根據上述表示方法,計算二進制加法 0.1 + 0.2 的結果爲:

0.1 + 0.2 = 10.0110011001100110011001100110011001100110011001100111 * 2^(-3) = 1.0011001100110011001100110011001100110011001100110100 * 2^(-2)
0.3 => 1.0011001100110011001100110011001100110011001100110011 * 2^(-2)

由於 0.1 + 0.2 在計算機底層存儲與 0.3 的底層存儲不一致,因此 0.1 + 0.2 == 0.3 爲 false

6. 常數比較運算

最後一種情況,fmt.Println(0.1+0.2 == 0.3) ,這源於 go 編譯器對於常數運算已經在編譯期就算好了,至於細節,有時間再細聊!

7. 結論

浮點數因爲在計算中會丟失精度,因此不能直接比較是否相等,而應該使用一個極小值,判斷兩個數的差值小於極小值時,即認爲二者相等!雖然某些時候,計算機誤打誤撞恰好判斷正確,但我們仍應該使用最穩妥的方式編程,避免引入不必要的 bug,而且要知道這種錯誤很難定位。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章