詳談IEEE浮點數編碼機制

lanzhiheng發表於2019-02-22

在一些工程領域中單單依靠整數是無法滿足他們對精度的需求的,這種時候就需要用到浮點數了。今天著重來聊一聊在計算機底層,浮點數的編碼方式,以及它相關值的計算方式。

二進位制小數

在介紹浮點數之前先來看看二進位制中實數可以如何表示。假設我有一個十進位制的小數8.33,那麼它的值可以表示為

8 + 3 / 10 + 3 / 100 = 833 / 100
複製程式碼

各個位的權重依次是10 ^ 0 = 1, 10 ^ -1 = 0.1, 10 ^ -2 = 0.01。其實二進位制小數也是類似的,只不過它是逢二進一。考慮這個二進位制串1001.1111,它所能夠代表的十進位制數字是多少呢?簡單地可以把它分成整數部分1001以及小數部分1111

整數部分

整數部分的計算很容易了,在不考慮符號的情況下依次代入相關的權重即可

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

(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浮點數是一個工業上的標準,根據標準來設計的機器彼此之間的相容性會比較高。

基本原理

IEEE浮點數的計算方式稍微有點麻煩,不過原理十分簡單,說白了就是科學計數法。假設我有一個十進位制小數100.2那麼其實這個數可以表示為0.1002 * (10 ^ 2),可以簡單地概括成這條公式N * (10 ^ K),把這條公式放到二進位制領域就是M * (2 ^ E)。其中M是該浮點數的尾數,主要影響浮點數的精度。E是浮點數的階碼,主要影響浮點數的大小。

IEEE浮點表示會把一個二進位制串分成3部分,分別用來儲存浮點數的尾數階碼以及代表浮點數正負的符號位。不過在IEEE浮點數中尾數以及階碼並不是直接儲存的,而是需要特殊的編碼方式。

不同精度的浮點數

IEEE浮點數主要分為單精度浮點數以及雙精度浮點數,分別對應C語言裡面的floatdouble兩種型別。

單精度浮點數通過32位的二進位制串來表示。其中0~22位(23位長)用來儲存尾數,23~30位(8位長)用來儲存階碼,第31位為最高有效位用來表示浮點數的符號。它的示意圖大概如下

float

雙精度浮點數所能夠表示的精度更大,範圍更廣。它用0~51位(52位長)用來儲存尾數,52~62位(11位長)用來儲存階碼,第63位為最高有效位用於表示浮點數的符號,它的示意圖大概如下

double

如今的C語言裡甚至還有long double這種資料型別,可以通過下面的程式碼來測試它的位元組長度

#include <stdio.h>

int main() {
  printf("%ld", sizeof(long double));
}
複製程式碼

在我的Mac上的執行結果是16個位元組長,也就是16 * 8 = 128位長。不過這種型別在不同的機器或者作業系統上表現可能會有所不同,移植性較差,不建議使用。

浮點的計算方式

簡單起見,這裡用32位的浮點數來詳細地講解一下IEEE浮點數值的相關計算方式。在32位二進位制串中,階碼部分用8位來儲存,尾數部分用23位來儲存,還有1位是符號位。

0. 偏置量與符號位

在講具體計算之前先來了解兩個特殊值,分別是偏置量以及符號位

偏置量Bias是一個用於計算階碼的特殊值。它的數值跟儲存階碼的位長有關,當階碼位長為k的時候偏置量的值為2 ^ k - 1,具體用途稍後會講到。另外一個需要注意的就是最高有效位,這個位利用原碼的相關知識,充當了浮點數的符號位。最高有效位為0的時候這個浮點數是正數,最高有效位為1的時候這個浮點數是負數。也就是說在IEEE浮點數中會出現+0.0或者-0.0這樣的數值。

二進位制位的不同“模式”將會有不一樣的數值計算方式,不過這兩個特殊值的理念在任何情況下幾乎是通用的,且容我一一道來。

1. 規格化浮點數

規格化浮點數的特點是儲存階碼的位既不全為0也不全為1,儲存尾數的位可以隨意定製。示意圖如下

Standard

可以推斷出它所能夠表示的無符號數取值範圍是1~254。這種情況下需要配合偏置量來求具體階碼E的值

E = e - Bias
複製程式碼

因此,階碼值的取值範圍是-126 ~ 127。接下來看尾數部分,在規格化的情況下尾數的位模式代表了小數點後面的數值,我們把這部分用f表示。不過這還不是真實的尾數,我們還需要把這個數值加1。於是有

M = 1 + f
複製程式碼

舉個例子,假設用於儲存尾數的位串是11000000000000000000000,那麼在規格化表示中,尾數的實際數值其實是1.11000000000000000000000。利用這些原理,嘗試計算下面的規格化數

1 01111111 10000000000000000000000
複製程式碼
  1. 最高有效位為1,所以該浮點數所表示的數值始終小於或等於0。
  2. 階碼部分以無符號的方式去解讀可得127,那麼實際階碼的值是E = e - Bias = 127 - 127 = 0
  3. 尾數部分在規格化數的計算中需要把數值加1來求得真實的尾數值,所以有M = 1 + f = 1 + 0.10000000000000000000000 = 1.10000000000000000000000

因此位串所代表的浮點數值是- 1.10000000000000000000000 * (2 ^ 0) = - (1 + 1/2) * 1 = - (3 / 2) = -1.5。其實並不是很難對吧?接下來看非規格化數的計算方式。

2. 非規格化浮點數

非規格化浮點數的特點就是用於儲存階碼的所有位全為0,儲存尾數的位可以隨意定製。非規格化浮點數主要用於表示那些非常接近於0的數。示意圖如下

NotStandard

只是它的計算方式跟規格化數相比還是有點區別的,在非規格化浮點表示中,用於儲存階碼的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. 最高有效位為0,所以該浮點數所表示的數值始終大於或者等於0。
  2. 作為一個非規格化數階碼部分全為0,因此階碼值始終是1 - Bias = 1 - 127 = -126
  3. 尾數部分直接表示了對應的尾數值M = f = 0.11100000000000000000000

因此位串所代表的浮點數值是0.11100000000000000000000 * (2 ^ -126) = (1 / 2 + 1 / 4 + 1 / 8) * (2 ^ -126) = (7 / 8) * (2 ^ -126)。這個值大概是等於1.0285575569695016e-38(我應該沒算錯吧^_^),這是一個非常小的數值。非規格化浮點數的計算方式與規格化差不多,只是處理起來稍稍有點特殊,這都是為了兩者間的平滑過渡。

3. 關於平滑過渡

我們可以通過具體示例來看看非規格化數與規格化數之間如何平滑地過渡,根據前面的原理我們還能得出一個結論,就是非規格數始終會比規格化數小。那麼他們之間的過渡便可以看成是從最大非規格化數過渡到最小規格化數了(假設數值都是大於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,講完了需要計算的東西,以及它們之間的平滑過渡,接下來再看一些不需要詳細計算的特殊值。

4. 特殊值

以上兩種表示方式已經能夠涵蓋大量的浮點數了,不過在某些情況下我們要有正無窮,負無窮,以及NaN這些特殊值來使程式設計更加簡便,那麼在IEEE浮點數中這些特殊值要如何表示呢?

a. 無窮

在IEEE浮點數中無窮的特徵是階碼的部分的位全為1,尾數部分的位全為0,示意圖如下

Infinite

前面已經談論過,最高有效位始終代表著浮點數的符號,用於標識正負。這個理論知識在無窮中依然有用。因此在這種模式下,最高有效位為0的時候表示正無窮

0 11111111 00000000000000000000000

最高有效位為1的時候表示負無窮

1 11111111 00000000000000000000000

b. NaN

另外一個特殊值是NaN,NaN翻譯過來就是Not A Number,它的特徵是階碼部分全為1的同時,尾數部分不全為0,示意圖如下

NaN

這種時候無論符號位是0還是1,它始終都是代表著NaN。

總結

這篇文章簡單地講解了一下IEEE浮點標準,並詳細談了如何去計算浮點數的具體數值,規格化與非規格化數的計算規則會有所不同,它們之間如何平滑過渡。當然,除非你參加一些計算機考試,不然一般不需要手動去計算這些數值。此外還講了像無窮,NaN這些特殊值的表示方式。雖然日常工作中這些知識用處不大,不過大家開心就好。

相關文章