原文地址:https://blog.fanscore.cn/p/26/
友情提示:本文排版不太好,但內容簡單,請耐心觀看,總會搞懂的。
1. 定點數
對於一個無符號二進位制小數,例如101.111
,如果我們要用2個位元組即16位來儲存它,我們可以約定用高8位儲存小數點前的數字,用低8位儲存小數點後的數字,這樣的話它在儲存空間中就是這樣的:00000101.11100000
。這種儲存方式中小數點的位置是固定的,這稱為定點數。這種儲存方式有個問題那就是儲存的數值是有上限的即11111111.11111111
= 27+26+25+24+23+22+21+20+2-1+2-2+2-3+2-4+2-5+2-6+2-7+2-8。如果我們要儲存1111111111111111.
這個數的話,用這個儲存方式是無法儲存的,但是實際上對於這個數來說16位的儲存空間是夠用的,就是說定點數存在空間浪費的缺點。
基於這個缺點,計算機中通常用浮點數來表示一個小數。
2. 浮點數
IEEE754標準使用V = (-1)s × M × 2E表示浮點數,符號位(sign)s 決定該數是正數(s=0)還是負數(s=1),尾數(significand)M是一個二進位制小數,階碼(exponent) E。
單精度浮點數中,s佔用1位,M佔用23位,E佔用8位,總共32位,雙精度浮點數s佔1位,M佔52位,E佔11位,總共64位,這兩種分別對應C中的float和double,另外還有一個擴充套件雙精度它佔用80位。
根據E值,浮點數有三種情況,
2.1 規格化的:E所有位既不全為0也不全為1。
在這種情況中,階碼被解釋為以偏置(biased)形式表示的有符號整數,這時E的值表示為E=e-Bias,其中e為E所佔位所表示的無符號整數,Bias=2E所佔位數-1。舉個單精度浮點數的?,假設當前E為00001010
那麼E = (00001010
所對應的無符號整數) - (28 - 1) = 10 - 127 = -117。
這種情況中M用來表示小數,其二進位制表示為1.f-1f-2f-3……fn。舉個單精度的例子,假設當前M為01100000000000000000100
,那麼M=1 + (2-2 + 2-3 + 2-21)。
2.2 非規格化的:E所有位都為0
在這種情況中,階碼值E=1-Bias,而尾數M二進位制表示為0.f-1f-2f-3……fn,沒有規格化值前面的1。
非規格化值有兩個用途。首先規格化值M始終>1,所以沒法表示0,所以+0.0的浮點表示的位模式為全0:符號位0,階碼欄位全為0(表明是一個非規格化值),尾數都是0就得到M=0.0。如果符號位為1,我們就得到了-0.0。其次非規格值的另外一個用途是表示那些非常接近0.0的數。
2.3 特殊值:E所有位都為1,這時又有兩種以下兩種情況
- 無窮大:M所有位全為0,當符號位為0是就是正無窮,當符號位為1時就表示負無窮。當我們把兩個特別大的數相乘或者除0的時候無窮能表示溢位的結果。
- NaN(Not a Number):M不全為0,如果一些運算的結果不能是實數或者無窮,比如對-1開平方根時就會返回NaN。
經過上面的講解後我們思考下十進位制數15.3203125
使用單精度浮點數來表示的話其二進位制形式應該是什麼呢?我們首先將它轉為二進位制數,即:1111.0101001
= 1.1110101001
× 23,即M=1.1110101001
,E=3。
3. 浮點數舍入
浮點數並不能表示所有的實數,比如十進位制的2.1沒有完全對應的二進位制數,浮點數只能近似的表示一些實數,為了儘量精確的表示這個實數就只能儘量增加二進位制的位數,但是資料型別的位數是有限的,比如C中float只有32位。
關於十進位制小數如何轉二進位制不清楚的同學可以自行搜尋下相關文章,很簡單,這裡就不詳述了。
這裡舉個例子:將十進位制的2.1用單精度浮點數表示。首先小數點前的2轉為二進位制是10,然後我們將小數點後的0.1轉為2進位制,它是這個樣子的:0.000110011001100110011001100110011001100110011001100110011...
(後面是0011無限迴圈)所以2.1轉為二進位制就是:10.000110011001100110011001100110011001100110011001100110011...
,轉為IEEE標準表達方式就是
1.0000110011001100110011001100110011001100110011001100110011... × 21,即M=0.0000110011001100110011001100110011001100110011001100110011... + 1,但單精度浮點數位數只有23位,這樣就面臨一個問題00001100110011001100110(這裡是23)01100110011001100110011001100110011...
這一長串23位之後的數字怎麼辦?直接捨去後面的位的話意味著計算機中所有小數都小於等於它的實際值,進1的話意味著計算機中所有小數都大於等於它的實際值,四捨五入看起來不錯,但是由於中間的5會進位,所以仍然會使計算系統中的小數整體偏大。在進行一些大量資料的統計時,這三種方式都回累計一個相當大的誤差。
IEEE浮點格式定義了四種不同的舍入方式,下面以十進位制的小數舍入只保留小數點後0位為例:
方式 | 1.40 | 1.60 | 1.50 | 2.50 | -1.50 |
---|---|---|---|---|---|
向偶數舍入 | 1 | 2 | 2 | 2 | -2 |
向零舍入 | 1 | 1 | 1 | 2 | -1 |
向下舍入 | 1 | 1 | 1 | 2 | -2 |
向上舍入 | 2 | 2 | 2 | 2 | -1 |
向偶數舍入這個方式乍看可能沒看懂,它其實是使舍入後的值的最低有效數字是偶數。1.5舍入有兩個選擇:1和2,但由於2是偶數所以就舍入到2,同樣2.5舍入有兩個選擇:2和3,但由於3是奇數,所以還是舍入到2。
向偶數舍入的方式使得在大多數情況下,5捨去還是進位的概率是差不多的,在進行一些大量資料的統計時產生的偏差相較其他方式小一些。
4. 二進位制舍入的?與規則總結
好多中文資料一般到這裡就戛然而止了,CSAPP書中講到這也沒有給到一個二進位制的例子,相信大部分讀者看完了上面也不知道二進位制裡是怎麼處理的,所以下面給個二進位制舍入的例子。
假設我們要求只保留小數點後三位,有以下例子:
1.001 011
舍入後:1.001
原因:1.001 011
舍入有兩個選擇:1.001
和1.010
,|1.001 011 - 1.001| = 0.000 011
,|1.001 011 - 1.010| = 0.000 101
,顯然0.000 011
<0.000 101
,所以1.001
比1.010
更接近原值1.001 011
,所以舍入到了1.001
1.001 101
舍入後:1.010
原因:1.001 101
舍入有兩個選擇:1.001
和1.010
,|1.001 101 - 1.001| = 0.000 101
,|1.001 101 - 1.010| = 0.000 011
,顯然0.000 101
>0.000 011
所以舍入到後者。1.001 100
舍入後:1.010
原因:1.001 100
舍入有兩個選擇:1.001
和1.010
,|1.001 100 - 1.001| = 0.000 100
,|1.001 100 - 1.010| = 0.000 100
,兩種選擇的差值是相同的,這時使用向偶數舍入的方式,1.010
是偶數(0偶1奇),所以舍入到1.010
根據上面的例子我們總結出以下規律:
我們用RR...RDD...D來表示一個二進位制小數,R表示保留位,D表示捨去位,那麼有以下規則:
- DD...D < 10...0 直接捨去
- DD...D > 10...0 向上舍入
- DD...D = 10...0 向偶數舍入,細則:
- RR...R = XX...0,直接捨去
- RR...R = XX...1,向上舍入
5. 程式碼驗證下
最後,我們寫一段C程式碼,看下到底是不是按照IEEE754標準存的浮點數,程式碼如下:
int main(void) {
float a = 2.1;
float b = a + 3;
return 0;
}
gcc編譯下:
$ gcc -O0 -g float.c // -O0禁用優化,-g以下面使用gdb除錯
gdb除錯下:
$ gdb ./a.out
進入gdb後,輸入start
再輸入layout asm
檢視反彙編結果:
可以看到a的值被存入了暫存器eax
,在gdb中通過i r eax
檢視eax暫存器中的值:
可以看到eax暫存器中儲存的值是0x400666666
,轉為二進位制:01000000000001100110011001100110
,套入IEEE754標準表示法:
0
10000000
00001100110011001100110
,即符號位為0,M = 1.00001100110011001100110
,E = 27 - (27 - 1) = 1