前言
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.
此前使用Java寫Spark SQL業務時,也有遇到浮點數比較問題即x>70的記錄行居然出現了70的記錄,盡管SQL做了類型轉換再比較也無濟於事....
因此了解浮點數是很有必要的喲~~
什么是浮點數
電氣和電子工程師協會IEEE對於計算機浮點數的存儲、運算、表示等推出了IEEE754標准!
標准中規定:
float32位單精度浮點數在機器中表示用 1 位表示數字的符號,用 8 位表示指數,用 23 位表示尾數。
double64位雙精度浮點數,用 1 位表示符號,用 11 位表示指數,52 位表示尾數。
其中指數域也稱為階碼。浮點數存儲字節定義如圖:
浮點數正規化
尾數不為0時,尾數域的最高有效位為1,這稱為規格化。否則,以修改階碼同時左右移動小數點位置的辦法,使其成為規格化數的形式。
浮點數x真值表示:
x=(−1)S×(1.M)×2e
float: e=E−127
double: e=E−1023
- S 符號位 0表示正 1表示負
- e 指數位 階碼E減去移碼
- M 尾數位 二進制形式移碼
移碼
移碼是真值補碼的符號位取反,一般用作浮點數的階碼,目的是便於浮點數運算時的對階操作。
對於定點整數,計算機一般采用補碼的來存儲。
正整數的符號位為0,反碼和補碼等同於原碼。
負整數符號位都為1,原碼,反碼和補碼的表示都不相同,由負數原碼表示法變成反碼和補碼有如下規則:
(1)原碼符號位為1不變,整數的每一位二進制數位求反得反碼;
(2)反碼符號位為1不變,反碼數值位最低位加1得補碼。
比如,以一個字節來表示-3,那么[−3]原=10000011 [−3]反=11111100 [−3] 補=11111101 [−3]移=01111101
舉個栗子
【3.14的單精度浮點數表示】
首先將3.14轉成二進制:
整數部分3的二進制是11
小數部分0.14的二進制是:0.0010001111010111000010[10001111.....](方括號中表示小數點后第23位及之后)
這樣,3.14的二進制代碼就是:11.0010001111010111000010[10001111....]×20
那么用正規化表示就是:1.10010001111010111000010[10001111....]×21
方括號表示的超出23位之后的二進制部分,由於單精度浮點數尾數只有23位,所以需要舍入(舍入方法見后)
由於第24位為1,且之后 不全為 0,所以需要向第23位進1完成上舍入:1.10010001111010111000011×21
而其指數是1,需要加上移碼127,即128,也就是1000 0000
它又是正數,所以符號為0
綜上所述,3.14的單精度浮點數表示為:
0 1000-0000 1001-0001-1110-1011-1000-011S符號位 0
e指數位 1000-0000
M尾數位 1001-0001-1110-1011-1000-011
十六進制代碼為:0x4048F5C3
誤差
通過栗子可知,3.14的單精度浮點數表示是0 1000-0000 1001-0001-1110-1011-1000-011。現在我們來還原,看看它的誤差:
指數是128,那么還原回去(減去移碼),實際指數就是1
尾數還原也就是:10010001111010111000011,所以正規化形式是:1.10010001111010111000011×21
也就是11.0010001111010111000011
利用二進制轉十進制,可得它對應的十進制數是:3.1400001049041748046875 不等於3.14
這就是為什么浮點數運算結果在業務代碼中總是不可確切預期的原因!!!!
機器ε
機器ε表示1與大於1的最小浮點數之差。例如雙精度表示1和表示大於1的最小浮點數
雙精度浮點數的機器ε = 2-52 ≈ 2.220446049250313e-16
同理,單精度的機器ε = 2-23 ≈ 1.1920928955078125e-7
在舍入規則中,相對舍入誤差不能大於機器ε的一半。
非正規化
單精度浮點數為例
(1)0的表示
對於階碼為0或255的情況,IEEE754標准有特別的規定:
如果 階碼E=0並且尾數M是0,則這個數的真值為±0(正負號和數符位有關)。
+0的機器碼為:0 00000000 000 0000 0000 0000 0000 0000
-0的機器碼為:1 00000000 000 0000 0000 0000 0000 0000
需要注意一點,浮點數不能精確表示0,而是以很小的數來近似表示0。因為浮點數的真值等於
x=(−1)S×(1.M)×2e
e=E−127
那么
+0的機器碼真值為 1.0×2−127
-0機器碼真值為 −1.0×2−127
(2)無窮的表示
如果階碼E=255 並且尾數M全是0,則這個數的真值為±∞(同樣和符號位有關)。
因此
+∞的機器碼為:0 11111111 000 0000 0000 0000 0000 0000
-∞的機器嗎為:1 11111111 000 0000 0000 0000 0000 0000
(3)NaN(Not a Number)
如果 E = 255 並且 M 不全是0,則這不是一個數(NaN)。
舍入規則
以23位尾數位的單精度浮點數為例,舍入時需要重點參考第24位
若第24位為1,且第24位之后全部為0。此時就要使第23位為0:若第23位本來就是0則不管,若第23位為1,則第24位就要向第23位進一位,這樣第23位就可以為0
若第24位為1,且第24位之后不全為0,則第24位就要向第23位進一完成上舍入。
若第24位為0,此時直接舍去不進位,稱為下舍入。
再來個栗子
JavaScript console 雙精度浮點數
>>9.4 - 9 - 0.4 === 0
<<false
>>(9.4-9-0.4).toFixed(20)
<<"0.00000000000000033307"9.4-9-0.4不嚴格等於0,其運算結果誤差。
因為按照上面的浮點數知識可知
9.4在機器內被表示為:9.4+0.2×2-49
0.4被表示為:0.4+0.1×2-52
當9.4-9時(因為9是整數是可以精確存儲的)得0.4+0.2×2-49,再減去0.4+0.1×2-52得3×2-53,約等於"0.00000000000000033307"。
詳細解釋:
9的二進制是1001,而0.4的二進制是0.0110-0110-0110-……無限循環的。從而9.4的二進制是1001.0110-0110……,正規化以后就變成 1.001-0110-0110-……×2^3,
因為雙精度浮點數是52位尾數,所以小數部分保留0.001-0110-0110-……-0110-0 [110-0110-0110-……]。即001后跟12個0110循環節,然后第52位是0,中括號表示從
第53位起開始舍棄的部分。根據我提到的舍入規則,第53位1且后面不全為0,要向第52位完成上舍入,所以小數部分就變成 0.001-0110-0110-……-0110-1。至此我們
可以看到,這個數較之9.4,由於小數部分第52位由0變為1,所以多加了2-52,但是因為從小數部分第53位開始舍棄了,舍棄部分是 0.1100-1100-…×2-52 = 0.8×2-52。
所以我們多加了2-52,但是少了0.8×2-52,這就意味着,但考慮尾數部分,這個數比9.4多了 2-52 - 0.8×2-52 = 0.2×2-52,別忘記之前還有一個2^3,所以整
體多了0.2×2-52×2^3 = 0.2×2-49
這就是為什么9.4在機器內被表示為:9.4+0.2×2-49
同理,0.4在機器內被表示為:0.4+0.1×2-52