所以要趕緊補充一些高大上的浮點數知識吧
浮點數很重要Go語言之父,Rob Pike大神曾經在微博吐槽過:不能掌握正則表達式或浮點數就不配當碼農!
雖然原文已經被刪除了(大神也有害怕的時候),還好我已經存檔了:
You should not be permitted to write production code if you do not have an journeyman license in regular expressions or floating point math.
關於正則表達式已經有很多權威著作或入門教程(但是很多主流的程式語言的實現居然也藏了不少爛算法,性能被幾十年前ed指數級吊打)。
但是討論浮點數的資料則比較少。一般的編程圖書也不會深入討論浮點數。但是這並不代表浮點數不重要。實際上IEEE754之父,William Kahan正是因為浮點數標準化的工作獲得了圖靈獎。
為什麼叫浮點數人的生命是有限的,計算機的內存也是有限的。在主流的程式語言中,浮點數一般有32bit和64bit兩種,分別對應單精度和雙精度浮點數(有些地方還有半精度的浮點數),也就是說一個32bit的內存只能表達2^32種狀態(大概40億)!
但是實際上數從無窮小到無窮大範圍異常的廣泛。一個阿伏伽德羅常數要寫成60221407600000000000000,十進位都要24位,32bit的二進位位根本不夠用。此外像原子半徑之類的數又非常之小,前面要用一垃圾0來填充。
為了少寫很多0(因為紙張有限,內存也只有有限的32比特),懶惰的先賢們發明了科學記數法來表示很大或很小的數。科學記數法的思路就是用較少的指數來表示很大或很小數的係數。但是光有科學記數法還不行,比如60221407600000000000000*10^0依然沒有節省紙張。只有規範化的科學記數法才能少寫垃圾0,比如6.02*10^23看起來就很清爽了。而規範化的科學記數法中,有效位部分的小數點是隨著指數發生變化的。
浮點數也是採用類似規範化的科學記數法的思路。而IEEE754是浮點數格式的國際標準,目前主流的程式語言都是採用這個標準。
浮點數的布局這是C語言表示的float浮點數內存布局:
union ieee754_float {
float f;
struct {
unsigned int mantissa:23;
unsigned int exponent:8;
unsigned int negative:1;
} ieee;
struct {
unsigned int mantissa:22;
unsigned int quiet_nan:1;
unsigned int exponent:8;
unsigned int negative:1;
} ieee_nan;
};
其中最低的23it是mantissa對應有效數字,然後是8bit的exponent表示指數,最高位的1bit表示符號位。需要注意的是指數部分採用移碼表示,也就是exponent作為無符號數減去127得到的最終的指數。
為何float32隻有6個精度因為有效數字mantissa部分是23bit,而1<<23對應十進位的8388608,不能完整表示全部的7個十進位位,因此就只剩下6個有效數字了。因此fmt.Printf中的%f對應float32默認就只列印6個十進位數(因為多列印就超出mantissa的表示範圍,不能準確表達)。
浮點數的詭異:浮點數有2個0知道了浮點數的布局,就自然可以理解為什麼浮點數會有2個0。因為浮點數有一個獨立的符號位,只要吧0.0的符號位設置為負數就可以得到一個負數0:
fmt.Println(math.Float32frombits(0))
fmt.Println(math.Float32frombits(1<<31))
上面的代碼通過在0的基礎之上加1<<31將符號位設置為1,這樣就得到了負0。
因此用浮點數作為map的Key時,可能會遇到一些詭異的事件。比如-0能取到0對應的值嗎?
浮點數的詭異:沒有0.3第一次看到這個標題,有人可能有異議。證據在這裡:
fmt.Println(0.3)
但是作為一個槓精,我要說fmt.Println代碼是有問題的,它可能作弊了,比如:
func fmt.Println(x) {
if x == 0.3 {
println("0.3")
}
}
fmt.Println內部列印的是字符串「0.3」,不是浮點數的0.3!
實際上fmt包真的是作弊了(而且作弊的算法也成了一門學問),不信看下面這個代碼:
func main() {
fmt.Printf("%f\n", 0.3)
fmt.Printf("%.10f\n", 0.3)
fmt.Printf("%.20f\n", 0.3)
}
每次輸出的結果都不一樣,這不是作弊是什麼?fmt包作弊列印0.3的原因是因為IEEE754中不存在0.3這個數!
不相信的話可以換成0.5試試:
func main() {
fmt.Printf("%f\n", 0.5)
fmt.Printf("%.10f\n", 0.5)
fmt.Printf("%.20f\n", 0.5)
}
輸出0.5時,就不會因為精度的變化導致輸出結果發生抖動。
對於Go語言中,常量和變量的運算規則是不同的,因此0.1+0.2換成變量就會發生變化:
func main() {
var a = 0.1
var b = 0.2
fmt.Println(0.1+0.2, a+b)
}
第一個輸出是0.3,第二個居然不是0.3。還好我們已經知道沒有0.3這個浮點數,因此第一個0.1+0.2的結果也不是0.3。不能表示0.3的原因是因為計算機採用的是二進位的科學記數法,而0.3無法通過用2的不同指數的狀態組成得到。
不僅沒有0.3,浮點數中缺少的數字多了去了。根據抽屜原理,float32隻能表示2的32次方個狀態,也就是40多億個數。但是float32的值域可是已經大大超出了40億的範圍,因此裡面必然有很多數在吃空餉。
無窮有正負無窮在浮點數中對應一個特殊的數(或者說指數值最大的那種0)。參考前面的浮點數內存布局,指數有8個bit表示,最大的指數表示是255。
下面我們構造一個指數部分是255的0:
fmt.Println(math.Float32frombits(255<<23))
這樣得到的就是一個正的無窮(255最大的指數表示窮,0有效位表示無)。
有了正無窮,得到負無窮就比較容易了。只要加一個符號標誌位即可:
fmt.Println(math.Float32frombits(255<<23 + 1<<31))
用科學記數法表示是這樣:0.0*2^(255-127)
Nan不是一個數,它表示的是一種bit模式在IEEE754浮點數的指數值域中255是最重要的一個,因為無窮對應的指數就是255。在無窮中除了指數是255之外,有效位部分是0。那麼問題來了,指數為255,有效位不是0的是啥玩意?
可以跑代碼看看:
fmt.Println(math.Float32frombits(255<<23 + 1))
fmt.Println(math.Float32frombits(255<<23 + 2))
它們都是NaN,也就是Not-a-Number,非數不是數。NaN不是一個非數,而是一類非數。
Nan有個重要的特性,就是自己和自己都不相等(想想如果用它作為map的key會有什麼效果)。Nan是非法的運算得來的,比如sqrt(-1)或0/0都是非數。
浮點數不滿足結合率浮點數不滿足結合率,比如a+(b+c)和(a+b)+c不等價。具體原因和開頭的x=x+1方程類似。
如果x=x+1成立,但是x=x+2並不一定成立。如果x!=x+2,那麼顯然它和x=(x+1)+1結果是不等價的。
四捨五入是錯誤的四捨五入分為2個部分,四舍還是四舍,但是五入就不一定是五入了。還存在五舍的情形。五作為一個絕對中間的位置,憑什麼總是要五入(借錢的人和還錢的人肯定也有不同的看法)?
那什麼時候需要五舍?根據計算的結果長得是否漂亮,選擇五舍或五入。
指數為什麼要採用移碼在早年間,浮點數運算晶片是一個奢侈的玩意。碼農們都不做浮點數運算。但是運氣也有背的時候,比如需要給一個浮點數表示的數組排序。
正如凌凌漆所言,IEEE754並非浪得虛名。即使沒有浮點數晶片,碼農們依然可以快速給浮點數數組排序:將浮點數數組當中整數數字進行排序就可以了。
為何?因為浮點數數組有序的話,那麼同樣bit模式的整數數組依然是有序的。其中第一個原因是符號位保證有序,第二個原因是指數採用移碼保證大指數較大,最後幾十有效數字部分比較小數點部分大數字。
解浮點數方程: x+1=x不是純數學意義上的方程, 對應計算機的一個浮點數問題:
if((float)(x+1.0) == (float)(x)) { x = ? }
簡單分析, ieee754中float採用23bit表示有效位, 再加省略的1, 共有24bit.
當結果超出24bit時, 小數部分被被丟失:
var a float32 = 1 << 24
var b float32 = a+1
fmt.Println(math.Float32bits(a))
fmt.Println(math.Float32bits(b))
因此浮點數運算是不滿足結合率的!
浮點數分類首先是符號位,根據符號位可以分為正數和負數兩類(包括兩種0)。不過符號位分類比較簡單,這裡忽略。
此外,根據指數位和有效數字部分的不同組合,可以分為以下幾種:
switch {
case 指數 在 1-254 之間:
case 指數 == 0:
if 有效數 == 0 {
} else {
}
case 指數 == 255:
if 有效數 == 0 {
} else {
}
}
所以說指數部分是IEEE754的精髓。
浮點數在數軸的分布