在一些工程領域中單單依靠整數是沒法知足他們對精度的需求的,這種時候就須要用到浮點數了。今天着重來聊一聊在計算機底層,浮點數的編碼方式,以及它相關值的計算方式。編程
在介紹浮點數以前先來看看二進制中實數能夠如何表示。假設我有一個十進制的小數8.33
,那麼它的值能夠表示爲bash
8 + 3 / 10 + 3 / 100 = 833 / 100
複製代碼
各個位的權重依次是10 ^ 0 = 1
, 10 ^ -1 = 0.1
, 10 ^ -2 = 0.01
。其實二進制小數也是相似的,只不過它是逢二進一。考慮這個二進制串1001.1111
,它所可以表明的十進制數字是多少呢?簡單地能夠把它分紅整數部分1001
以及小數部分1111
。測試
整數部分的計算很容易了,在不考慮符號的狀況下依次代入相關的權重便可ui
1 * 2 ^ 3 + 0 * 2 ^ 2 + 0 * 2 ^ 1 + 1 * 2 ^ 1 = 9
複製代碼
小數部分其實相似,只不過相關的權重須要稍微調整一下。編碼
1 * (2 ^ -1) + 1 * (2 ^ -2) + 1 * (2 ^ -3) + 1 * (2 ^ -4) = 1 / 2 + 1 / 4 + 1 / 8 + 1 / 16 = 15 / 16
複製代碼
再換個角度去看這個小數的部分,其實它還等價於1111 / 10000
。spa
(1 * 2 ^ 3 + 1 * 2 ^ 2 + 1 * 2 ^ 1 + 1 * 2 ^ 0) / (2 ^ 4) = 15 / 16
複製代碼
就是先計算二進制串1111
的值,再把它的小數點往左移動4
位,所以須要除以2 ^ 4
。結合整數部分以及小數部分,能夠獲得最終結果9 + (15 / 16) = 159 / 16
。操作系統
若是用已有的二進制編碼知識來表示數值159 / 16
,那麼只須要分配一段內存區域來存儲159
的二進制串10011111
,而後再利用另外一段區域來存儲小數點相關的偏移量00000100
便可。不過這種方式靈活性比較低,所可以表示的數值範圍也十分有限。那麼接下來看看現代機器中浮點數是如何表示的。翻譯
基於前面所談到的原理,瞭解IEEE浮點數就不是什麼大問題了。IEEE浮點數是一個工業上的標準,根據標準來設計的機器彼此之間的兼容性會比較高。設計
IEEE浮點數的計算方式稍微有點麻煩,不過原理十分簡單,說白了就是科學計數法。假設我有一個十進制小數100.2
那麼其實這個數能夠表示爲0.1002 * (10 ^ 2)
,能夠簡單地歸納成這條公式N * (10 ^ K)
,把這條公式放到二進制領域就是M * (2 ^ E)
。其中M是該浮點數的尾數,主要影響浮點數的精度。E是浮點數的階碼,主要影響浮點數的大小。3d
IEEE浮點表示會把一個二進制串分紅3部分,分別用來存儲浮點數的尾數,階碼以及表明浮點數正負的符號位。不過在IEEE浮點數中尾數以及階碼並非直接存儲的,而是須要特殊的編碼方式。
IEEE浮點數主要分爲單精度浮點數以及雙精度浮點數,分別對應C語言裏面的float
和double
兩種類型。
單精度浮點數經過32位的二進制串來表示。其中0~22位(23位長)用來存儲尾數,23~30位(8位長)用來存儲階碼,第31位爲最高有效位用來表示浮點數的符號。它的示意圖大概以下
雙精度浮點數所可以表示的精度更大,範圍更廣。它用0~51位(52位長)用來存儲尾數,52~62位(11位長)用來存儲階碼,第63位爲最高有效位用於表示浮點數的符號,它的示意圖大概以下
現在的C語言裏甚至還有long double
這種數據類型,能夠經過下面的代碼來測試它的字節長度
#include <stdio.h>
int main() {
printf("%ld", sizeof(long double));
}
複製代碼
在個人Mac上的運行結果是16個字節長,也就是16 * 8 = 128
位長。不過這種類型在不一樣的機器或者操做系統上表現可能會有所不一樣,移植性較差,不建議使用。
簡單起見,這裏用32位的浮點數來詳細地講解一下IEEE浮點數值的相關計算方式。在32位二進制串中,階碼部分用8位來存儲,尾數部分用23位來存儲,還有1位是符號位。
在講具體計算以前先來了解兩個特殊值,分別是偏置量以及符號位。
偏置量Bias是一個用於計算階碼的特殊值。它的數值跟存儲階碼的位長有關,當階碼位長爲k的時候偏置量的值爲2 ^ k - 1
,具體用途稍後會講到。另一個須要注意的就是最高有效位,這個位利用原碼的相關知識,充當了浮點數的符號位。最高有效位爲0的時候這個浮點數是正數,最高有效位爲1的時候這個浮點數是負數。也就是說在IEEE浮點數中會出現+0.0或者-0.0這樣的數值。
二進制位的不一樣「模式」將會有不同的數值計算方式,不過這兩個特殊值的理念在任何狀況下幾乎是通用的,且容我一一道來。
規格化浮點數的特色是存儲階碼的位既不全爲0也不全爲1,存儲尾數的位能夠隨意定製。示意圖以下
能夠推斷出它所可以表示的無符號數取值範圍是1~254
。這種狀況下須要配合偏置量來求具體階碼E
的值
E = e - Bias
複製代碼
所以,階碼值的取值範圍是-126 ~ 127
。接下來看尾數部分,在規格化的狀況下尾數的位模式表明了小數點後面的數值,咱們把這部分用f表示。不過這還不是真實的尾數,咱們還須要把這個數值加1。因而有
M = 1 + f
複製代碼
舉個例子,假設用於存儲尾數的位串是11000000000000000000000
,那麼在規格化表示中,尾數的實際數值實際上是1.11000000000000000000000
。利用這些原理,嘗試計算下面的規格化數
1 01111111 10000000000000000000000
複製代碼
127
,那麼實際階碼的值是E = e - Bias = 127 - 127 = 0
。M = 1 + f = 1 + 0.10000000000000000000000 = 1.10000000000000000000000
所以位串所表明的浮點數值是- 1.10000000000000000000000 * (2 ^ 0) = - (1 + 1/2) * 1 = - (3 / 2) = -1.5
。其實並非很難對吧?接下來看非規格化數的計算方式。
非規格化浮點數的特色就是用於存儲階碼的全部位全爲0,存儲尾數的位能夠隨意定製。非規格化浮點數主要用於表示那些很是接近於0的數。示意圖以下
只是它的計算方式跟規格化數相比仍是有點區別的,在非規格化浮點表示中,用於存儲階碼的8位全爲0,所以它所表示的無符號數始終爲0。這個時候偏置量Bias依然是2 ^ 8 - 1 = 127
。不過階碼值E
的計算方式倒是
E = 1 - Bias
複製代碼
而不是E = 0 - Bias
,這有點違反直覺。不過這都是爲了跟規格化浮點數進行一個平滑過渡。
接下來看尾數部分,在規格化浮點數中尾數部分所表示的數值始終須要加1,這種時候尾數的範圍是1 <= M < 2
。而在非規格化表示中尾數部分直接就是存儲了真實的尾數值,不須要再進行別的運算了,因而有
M = f
複製代碼
這時尾數的範圍是0 <= M < 1
。所以在非規格化表示中尾數部分10000000000000000000000
所對應的尾數值就是0.10000000000000000000000
利用這些原理來計算一個非規格化浮點數
0 00000000 11100000000000000000000
複製代碼
1 - Bias = 1 - 127 = -126
。M = f = 0.11100000000000000000000
所以位串所表明的浮點數值是0.11100000000000000000000 * (2 ^ -126) = (1 / 2 + 1 / 4 + 1 / 8) * (2 ^ -126) = (7 / 8) * (2 ^ -126)
。這個值大概是等於1.0285575569695016e-38
(我應該沒算錯吧^_^),這是一個很是小的數值。非規格化浮點數的計算方式與規格化差很少,只是處理起來稍稍有點特殊,這都是爲了二者間的平滑過渡。
咱們能夠經過具體示例來看看非規格化數與規格化數之間如何平滑地過渡,根據前面的原理咱們還能得出一個結論,就是非規格數始終會比規格化數小。那麼他們之間的過渡即可以當作是從最大非規格化數過渡到最小規格化數了(假設數值都是大於0的)。
利用前面所講過的浮點數的原理,我簡單地用一個8位二進制串來看這個過渡的過程,假設最高有效位爲符號位,其中3位存儲階碼,4位存儲尾數。在數值大於0的狀況下,最大非規格化數表示爲
0 000 1111
複製代碼
最小規格化數是
0 001 0000
複製代碼
這個時候你們偏置量都是2 ^ 3 - 1 = 7
。若是咱們的非規格化數00001111
的階碼部分按照E = 0 - e
來計算的話,那麼此時的非規格化數的值就是(2 ^ (0 - 7)) * (15 / 16) = 15 / 2048
。而規格化數00010000
的值是(2 ^ (1 - 7)) * 1 = 16 / 1024 = 32 / 2048
。彷佛差點意思對吧?
但若是按照標準的計算方式,用E = 1 - e
來計算非規格化數階碼的話,此時浮點數00001111
的數值是(2 ^ (1 - 7)) * (15 / 16) = 15 / 1024
。這個時候它跟最小規格化數值16 / 1024
的差距就很是小了,這就是所謂的平滑過渡。
OK,講完了須要計算的東西,以及它們之間的平滑過渡,接下來再看一些不須要詳細計算的特殊值。
以上兩種表示方式已經可以涵蓋大量的浮點數了,不過在某些狀況下咱們要有正無窮,負無窮,以及NaN這些特殊值來使編程更加簡便,那麼在IEEE浮點數中這些特殊值要如何表示呢?
在IEEE浮點數中無窮的特徵是階碼的部分的位全爲1,尾數部分的位全爲0,示意圖以下
前面已經談論過,最高有效位始終表明着浮點數的符號,用於標識正負。這個理論知識在無窮中依然有用。所以在這種模式下,最高有效位爲0的時候表示正無窮
0 11111111 00000000000000000000000
最高有效位爲1的時候表示負無窮
1 11111111 00000000000000000000000
另一個特殊值是NaN,NaN翻譯過來就是Not A Number
,它的特徵是階碼部分全爲1的同時,尾數部分不全爲0,示意圖以下
這種時候不管符號位是0仍是1,它始終都是表明着NaN。
這篇文章簡單地講解了一下IEEE浮點標準,並詳細談了如何去計算浮點數的具體數值,規格化與非規格化數的計算規則會有所不一樣,它們之間如何平滑過渡。固然,除非你參加一些計算機考試,否則通常不須要手動去計算這些數值。此外還講了像無窮,NaN這些特殊值的表示方式。雖然平常工做中這些知識用處不大,不過你們開心就好。