IEEE浮點標準詳解
1 IEEE浮點數
1.1 格式
IEEE定義了多種浮點格式,但最常見的是三種類型:單精度、雙精度、擴充套件雙精度,分別適用於不同的計算要求。一般而言,單精度適合一般計算,雙精度適合科學計算,擴充套件雙精度適合高精度計算。一個遵循IEEE 754標準的系統必須支援單精度型別(強制型別)、最好也支援雙精度型別(推薦型別),至於擴充套件雙精度型別可以隨意。
長度 |
符號 |
指數 |
尾數 |
有效位數 |
指數偏移 |
說明 |
|
單精度 |
32位 |
1 |
8 |
23 |
24 |
127 |
有1個隱含位 |
雙精度 |
64位 |
1 |
11 |
52 |
53 |
1023 |
有1個隱含位 |
擴充套件雙精度 |
80位 |
1 |
15 |
64 |
64 |
16383 |
沒有隱含位 |
需要特別注意的是,擴充套件雙精度型別沒有隱含位,因此它的有效位數與尾數位數一致,而單精度型別和雙精度型別均有一個隱含位,因此它的有效位數比位數位數多一個。
為了強制定義一些特殊值,IEEE標準通過指數將表示空間劃分成了三大塊:最小值指數(所有位全置0)用於定義0和弱規範數,最大指數(所有位全值1)用於定義±∞和NaN(Not a Number),其他指數用於表示常規的數。這樣一來,最大(指絕對值)常規數的指數不是全1的,最小常規數的指數也不是0,而是1。
1.1.1 單精度型別
C/C++中的float、FORTRAN中的REAL*4、Visual Basic中的Single、Java中的float等均是單精度型別。VC6對float的支援是打了折扣的,雖然VC6使用float儲存資料,但在函式呼叫過程中,所有float都會被轉換為double。這導致在Vc6中,float的效率還不如double(多了層轉換),而且有些細微的差異。不過,在一般計算中問題不大,但最好避免在VC6中使用float。除非記憶體過於緊張,否則沒有什麼好處。
1)格式引數
定義一個數據結構描述單精度型別的存節:
typedef struct _FP_SIGLE
{
unsigned __int32 nFraction : 23;
unsigned __int32 nExponent : 8;
unsigned __int32 nSign : 1;
} FP_SINGLE;
下列語句輸出0.5的符號、指數和尾數:
float a = 0.5;
FLOAT_SINGLE* p = (FLOAT_SINGLE*)&a;
printf( "%d/n", p->nSign );
printf( "%d/n", p->nExponent - 127 ); // 注意要減去偏移
printf( "%d/n", p->nFraction );
輸出結果是0,-1,0。0.5是正數,因此符號位是0,它的二進位制表示是0.12,標準化後是 ,因此指數是-1,尾數部分全是0(那個1是隱含的)。
下面將一個實數格式化為單精度型別。例如:
-235.125= -100100101001.0012 = -1.001001010010012×211
所以符號位是1,指數是:
11+127=138=10001012
而尾數是001001010010010000000002。
2)表示範圍
指數 |
尾數 |
數值 |
|
最大數 |
0xFE |
0x7FFFFF |
3.4028234663852886E+38 |
最小數 |
0x01 |
0x000000 |
1.1754943508222875E-38 |
最小弱規範數 |
0x00 |
0x000001 |
1.4012984643248171E-45 |
3)有效數字
單精度型別有24位有效位,因此有效數字是0.301×24=7.2,即單精度型別有7~8位有效數字。
1.1.2 雙精度型別
C/C++中的double、FORTRAN中的REAL*8、Visual Basic中的Double、Java中的double等均雙是精度型別。
1)引數格式
定義一個數據結構描述雙精度型別的存節:
typedef struct _FP_DOUBLE
{
unsigned __int64 nFraction : 52;
unsigned __int64 nExponent : 11;
unsigned __int64 nSign : 1;
} FP_DOUBLE;
例如下列語句生成一個雙精度浮點數0.5:
BYTE k[8];
FLOAT_DOUBLE* p = (FLOAT_DOUBLE*)k;
p->nSign = 0;
p->nFraction1 = 0;
p->nFraction2 = 0;
p->nExponent = 1023 - 1; // 2-1
2)表示範圍
指數 |
尾數 |
數值 |
|
最大數 |
0x7FE |
0xFFFFFFFFFFFFF |
1.7976931348623157E+308 |
最小數 |
0x01 |
0x0000000000000 |
2.2250738585072014E-308 |
最小弱規範數 |
0x00 |
0x0000000000001 |
4.9406564584124654E-324 |
3)有效數字
雙精度型別有53位有效位,因此有效數字是0.301×53=15.9,即單精度型別有15~16位有效數字。
1.1.3 擴充套件雙精度型別
表面上看,常見的計算一般無需使用擴充套件雙精度型別,因此擴充套件雙精度型別很少見,但這很可能是個錯覺。一些浮點硬體(例如 Intel x87 FPU)將擴充套件雙精度型別作為內部格式,即在計算前,其他浮點格式均需轉換為擴充套件雙精度型別,然後才進行計算,在輸出計算結果時,又將擴充套件雙精度型別轉換為其他格式。甚至可以說,擴充套件雙精度型別是最常用的格式,只是一般使用者不常見到而已。在C/C++中,long double一般對應於擴充套件雙精度型別,但由於這不是C/C++標準中約定的(C/C++標準只約定long double的精度和範圍高於double),因此並不一定是,例如在VC6中,long double與double一樣是雙精度型別。
1)格式引數
定義一個數據結構描述擴充套件雙精度型別的存節:
typedef struct _FP_DOUBLE_EXT
{
unsigned __int64 nFraction;
unsigned __int16 nExponent : 15;
unsigned __int16 nSign : 1;
} FP_DOUBLE_EXT;
例如下列語句生成一個雙精度浮點數0.5:
BYTE k[10];
FLOAT_EXTENDED* p = (FLOAT_EXTENDED*)k;
p->nSign = 0;
p->nFraction = 0x8000000000000000; // 注意此處的63位(整數位)總是1!
p->nExponent = 16383 - 1; // 2-1
2)表示範圍
指數 |
尾數 |
數值 |
|
最大數 |
0x7FFE |
0xFFFFFFFFFFFFFFFF |
1.18973149535723176E+4932 |
最小數 |
0x0001 |
0x0000000000000000 |
3.36210314311209552E-4932 |
最小弱規範數 |
0x0000 |
0x0000000000000001 |
1.82259976594123730E-4951* |
3)精度
擴充套件雙精度型別有64位有效位,因此有效數字是0.301×64=19.2,即擴充套件雙精度型別有19~20位有效數字。
1.2 分類
前面已大致提及IEEE為了定義一些特殊值以提高特殊情形下的處理能力,通過指數將表示空間劃分成了三類。其實這只是大致劃分,實際上更為詳細的劃分如下表所示:
指數 |
隱含位 |
尾數 |
說明 |
|
111...111 |
QNaN |
1 |
1XX...XXX |
尾數高位為1,但尾數不為0 |
SNaN |
1 |
0XX...XXX |
尾數高位為0,但尾數不為0 |
|
無窮(∞) |
1 |
0 |
尾數不0 |
|
有限數 |
1 |
XXX...XXX |
指數非0、非全1 |
|
000...000 |
弱規範數 |
XXX...XXX |
沒有隱含位,尾數不是0 |
|
零(0) |
0 |
0 |
尾數為0 |
可以看出,IEEE定義了6類數(暫且認為NaN或∞是數吧,不然不好措辭):QNaN、SNaN、∞、有限數、0和弱規範數。如果考慮符號,那麼還可以劃分更多,例如∞還可以分為+∞和-∞。雖然這些數均有嚴格清晰的描述,但到底是多少種,卻不大說得清。你可以認為+∞和-∞是一類,也可以認為它們是兩類,還有SNaN和QNaN,雖然有時也統稱為NaN,但它們在用途上有較大區別。因此,IEEE的數到底有幾類,往往取決於你的觀點。
其實,還有一種型別,就是違反IEEE 754標準格式的數。由於符合標準的數已經佔據了全部單精度型別和雙精度型別所能表示的範圍,因此,違反標準格式只在擴充套件雙精度型別中出現,例如指數全1但隱含位卻是0、指數全0而隱含位是1。對於這種數,某些硬體系統和軟體系統可能也會處理(例如x87 FPU),但應該禁止使用這種超出標準的格式。
使用這些特殊的數有時是非常困難的,不同的系統有不同的方式,並非都遵循IEEE 754標準。例如,如果這些數參與運算,那麼結果是什麼?什麼時候應該觸發異常?觸發什麼異常?如何使用NaN?那些操作是非法的,而那些操作是合法的,但結果卻是NaN一類的東西?諸如此類。
使用特殊值(0除外)在某種程度上意味著進入灰色地帶。對於一些情形,IEEE 754標準並未做出規定,或者相應的規定在實際應用中不切實際。即使在IEEE 754標準規定得很好的情形下,一些系統也出於各種考慮,並不遵循。例如,IEEE 754標準對Sin(∞)沒有規定。對錶達式x!=y當x或y是NaN時的值做出的規定卻難以應用。如果執行IEEE 754標準則意味著,要麼浮點比較指令相當複雜,要麼編譯器在計算這個表示式時進行特殊處理。不管怎樣,都會導致效率降低,卻沒有明顯的益處(畢竟,NaN參與運算是極其罕見的情形,正常計算中是不應該出現的)。在NaN的格式中,IEEE 754標準提倡編譯器或硬體系統在NaN中加入一些資訊以支援除錯,但幾乎沒有系統響應這個倡導。
通過處理異常似乎可以避開這片暗礁,但有時是不行的。例如,當你搜索一個超越方程的根的時候。如果你不知道根的分佈區間(這是常有的事),那麼在搜尋過程中極可能遇到無窮、溢位、NaN和異常之類。如果程式要自動完成根的搜尋,它就必需能夠處理這些問題,在每次嘗試失敗後能重新設定初值。可見,有時候這些麻煩是無法避免的。
(1)有限數
在這幾類數中,有限數是最常用的,也是唯一以常規方式解釋的數。有限數的特徵是指數在最大值和最小值之間,且整數位恆是1。例如對於單精度型別,它的指數有8位,考慮偏移後,指數最大值是255,最小值是0,而有限數的指數就在[1,254]區間。整數位是隱藏的,恆是1。它的形式是:
±( 1 + f )×2E-OFFSET
其中,E是指數,OFFSET是指數偏移,1是被隱含的整數位,f是尾數其他部分。
有限數的使用除了遵循數學規則之外,沒有其它規則。當然,有些出於硬體考慮附加的限制是存在的,例如計算正弦函式的FSIN指令就對輸入的資料範圍施加了限制。
除了違反數學規則之外,在一般使用有限數的過程中,最常見的問題是溢位,即運算結果超出了有限數的表示範圍。浮點數長度越小這個問題越常見,例如單精度型別。不過,只要稍微注意一下,一般不是大問題。
(2)0
0的特徵是指數、尾數、整數位全0,只有符號位可能不是0。它的形式是:
±( 0 + 0 )×20-OFFSET
與數學中0無正負不同,IEEE 754標準定義的0有正負,即0有兩種:+0和-0。之所以如此,有幾個原因:
[1] 被零除通常產生無窮,而無窮有正負無窮兩類;
[2] CopySign函式可以無需特別處理;
以上每個原因都不是絕對要求(畢竟,數學上0無正負就意味著0可以沒有符號),但在軟硬體實現上給0加上符號卻帶來一些方便。不過,這也意味著比較指令需要特別注意,因為+0和-0應相等,而不是+0大於-0。
(3)弱規範數
若規範數的指數與0一樣是0,它整數位也是0,但尾數部分不是0。它的形式是:
±( f )×20-OFFSET
但此處的f不侷限於[0,1),而是(0,2)。
IEEE標準引入弱規範數的目的是實現一種稱為“逐漸下溢”的技術。在計算過程中,如果中間結果小於最小的有限數卻不是0(即出現下溢),當作0處理會導致計算終止(例如病態矩陣)。引入弱規範數以後,在0和最小的有限數之間相當一部分數可以表示為弱規範數,從而提高了計算能力。例如單精度型別最小的有限數是1.1754943508222875E-38,而最小的弱規範數是1.4012984643248171E-45。
(4)∞
∞的指數部分是最大值,整數位是1,尾數部分是0。它的形式是:
±( 1 + 0 )×2MAX-OFFSET
因此∞有兩類,即+∞和-∞。產生∞的一般情形有:
[1] ∞自身運算,例如-∞+1.0得到-∞;
[2] 被0除,例如1/+0得到+∞;
[3] 上溢,即計算結果超出了類型範圍,通過舍入得到∞。
由於在一般數學中,∞是不能參與運算的,因此IEEE的這些規定可以說是某種擴充套件。
(5)NaN
NaN的意思是Not a Number或者Not any number。NaN之所以顯得比較奇怪,是因為數學上本沒有這麼一個數或符號,它純粹是為了方便處理而提出來的,但它的歷史可不短。早在三十年代後期就有人提出了類似NaN的概念。1963年的CDC 6600系統實現了它,但將它視為“沒有定義”。後來,DEC的PDP-11和VAX系統也使用它,但將它用作“保留的運算元”。時至今日,雖然IEEE明確地定義了NaN,但在實際使用過程中,NaN經常被誤解誤用,需要特別小心。
與∞一樣,NaN的指數部分是最大值,整數位是1,但它尾數部分不是0。它的形式是:
±( 1 + f )×2MAX-OFFSET
其中,f≠0。
NaN有兩類,一類是QNaN(Quiet NaN),一類是SNaN(Signal NaN)。兩者的不同在於IEEE標準要求,如果SNaN參與運算要觸發非法操作異常,而QNaN參與運算可以不觸發異常。兩者在格式上的區別在於,QNaN的尾數最高位是1,而SNaN的尾數最高位是0。一般情形下,如果不特別宣告,NaN指的是QNaN。
IEEE標準引入NaN的目的是希望給編譯器等系統一個約定的值設定未初始化的資料,或者在計算出問題時可以返回一個東西提示計算出現了問題。
2 兩個問題
2.1 遵循IEEE標準?
在多大程度上遵循IEEE標準與目標系統的性質有關。如果目標系統是個面向廣泛使用者的、商業化的產品,例如公開發售的浮點晶片或某個語言的編譯器,那麼儘可能嚴格地遵循IEEE標準是必需的。如果目標系統中存在與IEEE標準相抵觸的特性,這些特性可能會給使用者的開發或其上的程式碼帶來問題,因為那些系統通常會假設目標系統是遵循IEEE標準的。但如果目標系統只是一個受到嚴格控制的、只在有限範圍內應用的系統,例如某款手持裝置的浮點模擬庫,使用者只是有限的幾個產品開發商,那麼全面遵循IEEE標準是不必要地耗費精力和金錢。對這類目標系統,如何達到效能指標和開發速度要求才是主要問題,試圖嚴格遵循IEEE標準會給系統開發帶來阻礙,而且沒有可觀的回報。有選擇地遵循一些IEEE標準的主要特性(例如浮點格式)、忽略那些幾乎不可能給目標系統帶來任何好處的特性(例如QNaN和SNaN的區分、NaN參與邏輯比較運算時的瑣碎約定)是這類系統的理性選擇。
舉個例子說明一些嚴格遵循IEEE標準可能會帶來的問題。例如IEEE建議的函式:
hypot( x, y ) = sqrt( x2 + y2 )
這個函式簡單的實現程式碼如下:
double hypoy( double x, double y )
{
return sqrt( x*x + y*y );
}
但這個實現不符合IEEE標準的要求。按照一些人對IEEE標準的理解,由於sqrt(x2+∞2)=+∞在x取任何有限數、弱規範數、∞時都成立,因此也要求sqrt(NaN2+∞2)=+∞成立。然而,在上述實現程式碼中,當x=NaN時,返回值是NaN。因此,需要對∞參與運算的情形作特殊處理。偽碼如下:
double hypoy( double x, double y )
{
if( isinfinite( x ) || isinfinite( y ) )
return infinite( 0 );
return sqrt( x*x + y*y );
}
這只是一個簡單的例子,許多函式的特殊情形處理遠比這個函式要複雜(參見附帶原始碼)。這些特殊處理程式碼給維護帶來困難,降低了程式碼的效率,而且看不出這些程式碼帶來了什麼好處。畢竟,數學上沒有NaN、數值分析中沒有NaN和∞,這些特殊情形處理程式碼在一般計算中幾乎沒有用處。而且,有些約定的返回值並不比返回其他值更有理由,為什麼需要特別新增這些程式碼呢?例如在上述程式碼中,實在看不出,當有∞參與運算(在數值分析中,這是不可能的,因為∞的出現就意味著計算出現了溢位,計算通常應該停止)時,返回+∞比返回NaN有什麼好處。甚至,由於返回+∞而不是NaN,掩蓋了NaN帶來的計算異常(未初始化資料、執行了非法操作等)警告,更為不妥。
當然,以上觀點只是一家之言。
2.2 弱規範數格式的解釋
無窮只參與有限的運算,沒有具體值,但弱規範數不同,它有具體的值而且像有限數一樣參與運算,因此如何解釋弱規範數的格式(即確定與它對應的數值)非常重要。前面將弱規範數記為:
±( f )×20-OFFSET
其中f是尾數部分,E是指數部分,OFFSET是指數偏移。需要特別指出的是,弱規範數沒有0,它的整數位就是尾數的最高位,因此f的取值範圍是[0,2),這導致弱規範數與有限數的解釋不一樣,相當晦澀,下面以雙精度型別為例說明一下。
假設若規範數有一個為0的隱含位,那麼2-1023將無法用任何方式記錄下來。因為如果用有限數格式,即:
sign = 0;
exponent = 0x000
fraction = 0x0000000000000
這個數竟然變成了0的格式!如果使用弱規範數表示(有一個隱含的0),即:
sign = 0;
exponent = 0x001
fraction = 0x8000000000000
可是既然它的指數不是0,它自然也就不能被視為弱規範數了。顯然,哪兒出了問題。問題就出在認為弱規範數有一個為0的隱含位。
如果認為弱規範數沒有隱含位,而是以最高尾數位作為整數位,那麼2-1023是:
sign = 0;
exponent = 0x000
fraction = 0x8000000000000
這意味著,當從弱規範數形式轉換為有限數形式時,不僅要向左移位以產生隱含位,而且在移位時,指數是不減1的(因為只是整數位移動,值不變)。下列程式碼取自VC6浮點庫frexp()反彙編程式碼。frexp()分解double型別的指數和尾數部分(以一個有限數形式返回)。它使用下列迴圈產生尾數部分的隱含位以及修正指數部分:
while( px->nExponent & 1 == 0 )
{
*(unsigned __int64*)&x = *(unsigned __int64*)&x << 1;
*expptr --;
}
通過向左移位尋找一個1設定隱含位,由於向左移位意味著乘以2,因此需要同時減小指數。問題的關鍵在於指數初值:
*expptr = -1021;
一個規範好的弱規範數的指數必然是-1023,由於frexp()返回的尾數部分需要顯示隱含位,因此尾數部分被除以了2(即通過右移一位以使隱含位出現),因此指數部分需要加1,由此得到-1022。由於上述迴圈每次移位指數均減1,但實際上最後一次移位時,只是將弱規範數的整數位設定為隱含位,沒有發生數值變化,不應該減1。也就是說,迴圈多減了一次。因此初值要補足一個1,於是得到-1021。
還有一個細節需要注意,就是弱規範數在單精度型別或雙精度型別與擴充套件雙精度型別之間進行轉換時,擴充套件雙精度的整數位來自尾數的最高位,而不是有限數中的隱含位。再次強調一下,無論在任何各格式中,若規範數沒有隱含位,尾數最高位就是它的整數位。