1. 程式人生 > >計算機記憶體中浮點數的表示

計算機記憶體中浮點數的表示

浮點概念的引入

在計算機系統的發展過程中,曾經提出過多種方法表達實數。比如定點數表示法, 這種表示方法將小數點的位置固定在某一個位置,比如: 11001000.00110001,這個16位(2位元組) 的定點數用前面8位表示整數部分,後面8位表示小數部分,這種方法直觀,但是固定的小數點位置決定了固定位數的整數部分和小數部分,不利於同時表達特別大的數或者特別小的數。最終,絕大多數現代計算機遵循IEEE754,即IEEE二進位制浮點數算數標準,利用科學計數法來表達實數,即用一個尾數(Mantissa or significand),一個基數(Base),一個指數(Exponent)以及一個表示正負的符號來表達實數。比如 123.45 用十進位制科學計數法可以表達為 1.2345 × 10^2 ,其中1.2345 為尾數,10 為基數,2為指數。浮點數利用指數達到了浮動小數點的效果,從而可以靈活地表達更大範圍的實數。

浮點數在記憶體中的格式

IEEE 754標準定義了五種浮點數格式,使用基數和該格式佔用的記憶體位元數命名,五種格式分別為:

  1. 二進位制32位浮點數 (binary32)
  2. 二進位制64位浮點數 (binary64)
  3. 二進位制 128位浮點數(binary128)
  4. 十進位制64位浮點數(decimal64)
  5. 十進位制128位浮點數(decimal128)

各種程式語言和編譯器一般都支援前兩種格式,分別對應於float和double,對於後一種二進位制高雙精度128位浮點數,一般編譯器使用80位表示,也有極個別的編譯器將他們視為128位。

在IEEE標準中,浮點數在記憶體中的表示是將特定長度的連續位元組的所有二進位制位按特定長度劃分為符號域,指數域和尾數域三個連續域。 例如,對於float型別的資料,其在記憶體中的表示為:

這裡寫圖片描述

對於double型別,其表示形式如下:

這裡寫圖片描述

從上面可以看出,float型別在記憶體中佔用的位數為: 1+8+23=32bits, double 型別在記憶體中佔用的位元數為: 1+11+52=64bits

  • 第一位s代表符號為,1代表負數,0代表正數。
  • 第二個域是指數域,對於單精度float型別,指數域有8位,可以表示 0-255個指數值。指數值規定了小數點的位置,小數點的移動代表了所表示數值的大小。但是,指數可以為正數,也可以為負數。為了處理負指數的情況,實際的指數值按要求需要加上一個偏差(Bias)值作為儲存在指數域中的值,單精度數的偏差 值為 -127,而雙精度double型別的偏差值為 -1023。比如,單精度指數域中的64 則表示實際的指數值 -63。 偏差的引入使得對於單精度數,實際可以表達的指數值的範圍就變成-127 到 128 之間(包含兩端)。我們不久還將看到,實際的指數值-127(儲存為 全 0)以及 +128(儲存為全1)保留用作特殊值的處理。這樣,實際可以表達的有效指數範圍就在 -126 和 +127 之間。
  • 第三個域為尾數域,其中單精度數為 23 位長,雙精度數為 52 位長。比如一個單精度尾數域中的值為: 00001001000101010101000, 第二個域中的指數值則規定了小數點在尾數串中的位置,預設情況下小數點位於尾數串首位之前。  比如指數值為 -1,則該float數即為:.000001001000101010101000,如果為+1,則該float 數值為:0.0001001000101010101000。我們知道引入浮點數的目的在於用盡可能少的位數表示既高精度又大範圍的實數,其中的範圍大小是由指數域位長確定的,而尾數域的長度則確定了所能表示實數的精度,所以double比float數的精度更高,範圍更大,相應的也就佔用更多的記憶體。 剛才我們介紹的對尾數域中的值的解釋並不能實現這個精度最大化的目標,因為在尾數串第一個”1”之前還有4個”0”,這4個”0”實際上是多餘的,因為我們把小數點向前移動時,前端的"0"是自動新增的,所以可以把這4個“0”刪除,然後尾數域末端多出4個位來表示更高精度的數值。也就是說尾數的第一位一定是"1",那麼既然第一位一定是"1",那麼我們也就沒有必要把它儲存在尾數域中,而是直接預設尾數為1.xxxx…xxx的形式。尾數的首位從小數點後開始。那麼上面的例子所表示的尾數就是:
    1.00001001000101010101000。 用23位表示了24位的資訊 (小數點不佔位置).

浮點數轉換成實數

我們已經明白了浮點數在記憶體中的儲存形式。那麼下面就用一個例子來強化一下我們對浮點數的理解。 假設我們記憶體中有一個32位的浮點數:
這裡寫圖片描述

解釋:
s: 1-表示負數
e: 10000010->130->(130-127)=3,也即尾數部分小數點向右移3位。
m: 10110000000000000000000,尾數加上前面省略的1和小數點為:1.10110000000000000000000.

現在三個部分都已經算出來了,通過指數值調整小數點的位置,得到結果為:
1101.10000000000000000000
轉換成10進位制,整數部分為:1×23+1×22+1×20=13; 小數部分為:1×21=0.5.
最後整數部分加上小數部分,再加上符號,所表示的實數就是:13.5

特殊值

我們已經知道,指數域實際可以表達的指數值的範圍為-127 到128 之間(包含兩端)。其中,值-127(儲存為 全 0)以及+128(儲存為全1)保留用作特殊值的處理。
浮點數中的特殊值主要用於特殊情況或者錯誤的處理。比如在程式對一個負數進行開平方時,一個特殊的返回值將用於標記這種錯誤,該值為NaN(Nota Number)。沒有這樣的特殊值,對於此類錯誤只能粗暴地終止計算。除了NaN 之外,IEEE標準還定義了±0,±∞ 以及非規範化數(DenormalizedNumber)。
對於單精度浮點數,所有這些特殊值都由保留的特殊指數值-127 和128 來編碼。NaN的指數為128(指數域全為1),且尾數域不等於零。IEEE 標準沒有要求具體的尾數域,所以NaN 實際上不是一個,而是一族。不同的實現可以自由選擇尾數域的值來表達NaN,比如Java 中的常量Float.NaN 的浮點數可能表達為01111111110000000000000000000000,其中尾數域的第一位為 1,其餘均為 0(不計隱藏的一位),但這取決系統的硬體架構。Java 中甚至允許程式設計師自己構造具有特定位模式的 NaN 值(通過Float.intBitsToFloat() 方法)。比如,程式設計師可以利用這種定製的 NaN 值中的特定位模式來表達某些診斷資訊。
和NaN一樣,特殊值無窮的指數部分同樣為128,不過無窮的尾數域必須全部為0。無窮用於表達計算中產生的上溢(Overflow)問題。比如兩個極大的數相乘時,儘管兩個運算元本身可以用儲存為浮點數,但其結果可能大到無法儲存為浮 點數,而必須進行舍入。根據 IEEE 標準,此時不是將結果舍入為可以儲存的最大的浮點數(因為這個數可能離實際的結果相差太遠而毫無意義),而是將其舍入為無窮。對於負數結果也是如此,只不 過此時舍入為負無窮,也就是說符號域為 1 的無窮。

因為IEEE 標準的浮點數格式中,小數點左側的1 是隱藏的,而零顯然需要尾數必須是零。所以,零也就無法直接用這種格式表達而只能特殊處理。
實際上,零儲存為尾數域為全為0,指數域為emin - 1 = -127,也就是說指數域也全為 0。考慮到符號域的作用,所以存在著兩個零,即 +0 和 -0。不同於正負無窮之間是有序的,IEEE 標準規定正負零是相等的。
零有正負之分,的確非常容易讓人困惑。這一點是基於數值分析的多種考慮,經利弊權衡後形成的結果。有符號的零可以避免運算中,特別是涉及無窮的運算 中,符號資訊的丟失。舉例而言,如果零無符號,則等式 1/(1/x) = x 當x = ±∞ 時不再成立。原因是如果零無符號,1 和正負無窮的比值為同一個零,然後1 與 0 的比值為正無窮,符號沒有了。解決這個問題,除非無窮也沒有符號。但是無窮的符號表達了上溢發生在數軸的哪一側,這個資訊顯然是不能不要的。零有符號也造 成了其它問題,比如當 x=y 時,等式1/x = 1/y 在 x 和 y 分別為 +0 和 -0 時,兩端分別為正無窮和負無窮而不再成立。當然,解決這個問題的另一個思路是和無窮一樣,規定零也是有序的。但是,如果零是有序的,則即使 if(x==0) 這樣簡單的判斷也由於可能是 ±0而變得不確定了。兩害取其輕者,零還是無序的好。