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,而且要知道這種錯誤很難定位。