1 浮點數的表示
通常,我們可以用下面的格式來表示浮點數
其中S是符號位,P是階碼,M是尾數
對於IBM-PC而言,單精確度浮點數是32位(即4位元組)的,雙精確度浮點數是64位(即8位元組)的。兩者的S,P,M所佔的位元以及表示方法由下表可知
S |
P |
M |
表示公式 |
位移量 |
1 |
8 |
23 |
(-1)S*2(P-127)*1.M |
127 |
1 |
11 |
52 |
(-1)S*2(P-1023)*1.M |
1023 |
以單精確度浮點數為例,可以得到其二進位的表示格式如下
S(第31位) |
P(30位到23位) |
M(22位到0位) |
其中S是符號位,只有0和1,分別表示正負;P是階碼,通常使用移碼錶示(移碼和補碼只有符號位相反,其餘都一樣。對於正數而言,原碼,反碼和補碼都一樣;對於負數而言,補碼就是其絕對值的原碼全部取反,然後加1.)
為了簡單起見,本文都只討論單精確度浮點數,雙精確度浮點數也是用一樣的方式儲存和表示的。
2 浮點數的表示約定
單精確度浮點數和雙精確度浮點數都是用IEEE754標準定義的,其中有一些特殊約定。
(1) 當P = 0, M = 0時,表示0。
(2) 當P = 255, M = 0時,表示無窮大,用符號位來確定是正無窮大還是負無窮大。
(3) 當P = 255, M != 0時,表示NaN(Not a Number,不是一個數)。
當我們使用.Net Framework的時候,我們通常會用到下面三個常量
Console.WriteLine(float.MaxValue); // 3.402823E+38
Console.WriteLine(float.MinValue); //-3.402823E+38
Console.WriteLine(float.Epsilon); // 1.401298E-45
//如果我們把它們轉換成雙精確度類型,它們的值如下
Console.WriteLine(Convert.ToDouble(float.MaxValue)); // 3.40282346638529E+38
Console.WriteLine(Convert.ToDouble(float.MinValue)); //-3.40282346638529E+38
Console.WriteLine(Convert.ToDouble(float.Epsilon)); // 1.40129846432482E-45
那麼這些值是如何求出來的呢?
根據上面的約定,我們可以知道階碼P的最大值是11111110(這個值是254,因為255用於特殊的約定,那麼對於可以精確表示的數來說,254就是最大的階碼了)。尾數的最大值是11111111111111111111111。
那麼這個最大值就是:0 11111110 11111111111111111111111。
也就是 2(254-127) * (1.11111111111111111111111)2 = 2127 * (1+1-2-23) = 3.40282346638529E+38
從上面的雙精確度表示可以看出,兩者是一致的。最小的數自然就是-3.40282346638529E+38。
對於最接近於0的數,根據IEEE754的約定,為了擴大對0值附近資料的表示能力,取階碼P = -126,尾數 M = (0.00000000000000000000001)2 。此時該數的二進位表示為:0 00000000 00000000000000000000001
也就是2-126 * 2-23 = 2-149 = 1.40129846432482E-45。這個數字和上面的Epsilon是一致的。
如果我們要精確表示最接近於0的數字,它應該是 0 00000001 00000000000000000000000
也就是:2-126 * (1+0) = 1.17549435082229E-38。
3 浮點數的精度問題
浮點數以有限的32bit長度來反映無限的實數集合,因此大多數情況下都是一個近似值。同時,對於浮點數的運算還同時伴有誤差擴散現象。特定精度下看似相等的兩個浮點數可能並不相等,因為它們的最小有效位元不同。
由於浮點數可能無法精確近似於十進位數,如果使用十進位數,則使用浮點數的數學或比較運算可能不會產生相同的結果。
如果涉及浮點數,值可能不往返。值的往返是指,某個運算將原始浮點數轉換為另一種格式,而反向運算又將轉換後的格式轉換回浮點數,且最終浮點數與原始浮點數相等。由於一個或多個最低有效位可能在轉換中丟失或更改,往返可能會失敗。
4 將浮點數表示為二進位
4.1 無小數的浮點數轉換成二進位表示
首先,我們用一個不帶小數的浮點數來說明如何將一個浮點數轉換成二進位表示。假設要轉換的資料是45678.0f。
在處理這種不帶小數的浮點數時,直接將整數部分轉化為二進位表示:
1011001001101110.0,這時要加上一位預設的1(這是因為按照浮點數規格化的要求,尾數必須化成 1.M的格式),
那麼可以表示成:11011001001101110.0。
然後將小數點向左移,一直移到離最高位只有1位,也就是 1.1011001001101110,一共移動了16位,我們知道,左移位表示乘法,右移位表示除法。所以原數就等於這樣:1.1011001001101110 * ( 216 )。現在尾數和指數都出來了。因為最高位的1是根據標準加上去的,只是為了滿足規格化的要求,這時候需要把這個1去掉。尾數的二進位就變成了:1011001001101110。
最後在尾數的後面補0,一直到補夠23位,就是:10110010011011100000000。
再回來看指數,根據前面的定義,P-127=16,那麼P = 143,表示成二進位就是:10001111。
45678.0f這個數是正的,所以符號位是0,那麼我們按照前面講的格式把它拼起來,就是:0 10001111 10110010011011100000000。
這就是45678.0f這個數的二進位表示,如果我們要得到16進位的表示,非常簡單,我們只需要把這個二進位串4個一組,轉換成16進位數就可以了。但是要注意的是x86架構的CPU都是Little Endian的(也就是低位位元組在前,高位位元組在後),所以在實際記憶體中該數字是按上面二進位串的倒序儲存的。要知道CPU是不是little endian的也很容易。
BitConverter.IsLittleEndian;
4.2 含小數的浮點數表示為二進位
對於含小數的浮點數,會有精度的問題,下面舉例說明。假設要轉換的小數為123.456f。
對於這種帶小數的就需要把整數部和小數部分開處理。對於整數部分的處理不再贅述,直接化成二進位為:100100011。小數部份的處理比較麻煩一些,我們知道,使用二進位表示只有0和1,那麼對於小數就只能用下面的方式來表示:
a1*2-1+a2*2-2+a3*2-3+......+an*2-n
其中a1等數可以是0或者1,從理論上將,使用這種表示方法可以表示一個有限的小數。但是尾數只能有23位,那麼就必然會帶來精度的問題。
在很多情況下,我們只能近似地表示小數。來看0.456這個十進位純小數,該如何表示成二進位呢?一般說來,我們可以通過乘以2的方法來表示。
首先,把這個數字乘以2,小於1,所以第一位為0,然後再乘以2,大於1,所以第二位為1,將這個數字減去1,再乘以2,這樣迴圈下去,直到這個數字等於0為止。
在很多情況下,我們得到的位元字都大於23位,多於23位的就要捨去。進位原則是0舍1入。通過這樣的辦法,我們可以得到二進位表示:1111011.01110100101111001。
現在開始向左移小數點,一共移了6位,這時候尾數為:1.11101101110100101111001,階碼為6加上127得131,二進位表示為:10000101,那麼總的二進位表示為:
0 10000101 11101101110100101111001
表示成十六進位是:42 F6 E9 79
由於CPU是Little Endian的,所以在記憶體中表示為:79 E9 F6 42。
4.3 將純小數表示成二進位
對於純小數轉化為二進位來說,必須先進行規格化。例如0.0456,我們需要把它規格化,變為1.xxxx * (2n )的形式,要求得純小數X對應的n可用下面的公式:
n = int( 1 + log 2X )
0.0456我們可以表示為1.4592乘以以2為底的-5次方的冪,即1.4592 * ( 2-5 )。轉化為這樣形式後,再按照上面處理小數的方法處理,得到二進位表示
1. 01110101100011100010001
去掉第一個1,得到尾數
01110101100011100010001
階碼為:-5 + 127 = 122,二進位表示為
0 01111010 01110101100011100010001
最後轉換成十六進位
11 C7 3A 3D
5 浮點數的數學運算
5.1 浮點數的加減法
設兩個浮點數 X=Mx*2Ex ,Y=My*2Ey
實現X±Y要用如下5步完成:
(1)對階操作:小階向大階看齊
(2)進行尾數加減運算
(3)規格化處理:尾數進行運算的結果必須變成規格化的浮點數,對於雙符號位(就是使用00表示正數,11表示負數,01表示上溢出,10表示下溢出)的補碼尾數來說,就必須是
001×××…×× 或110×××…××的形式
若不符合上述形式要進行左規或右規處理。
(4)舍入操作:在執行對階或右規操作時常用“0”舍“1”入法將右移出去的尾數數值進行舍入,以確保精度。
(5)判結果的正確性:即檢查階碼是否溢出
若階碼下溢(移碼錶示是00…0),要置結果為機器0;
若階碼上溢(超過了階碼錶示的最大值)置溢出標誌。
現在用一個具體的例子來說明上面的5個步驟
例題:假定X=0 .0110011*211,Y=0.1101101*2-10(此處的數均為二進位), 計算X+Y;
首先,我們要把這兩個數變成2進位表示,對於浮點數來說,階碼通常用移碼錶示,而尾數通常用補碼錶示。
要注意的是-10的移碼是00110
[X]浮: 0 1 010 1100110
[Y]浮: 0 0 110 1101101
符號位 階碼 尾數
(1)求階差:│ΔE│=|1010-0110|=0100
(2)對階:Y的階碼小,Y的尾數右移4位
[Y]浮變為 0 1 010 0000110 1101暫時儲存
(3)尾數相加,採用雙符號位的補碼運算
00 1100110
+00 0000110
00 1101100
(4)規格化:滿足規格化要求
(5)舍入處理,採用0舍1入法處理
故最終運算結果的浮點數格式為: 0 1 010 1101101
即X+Y=+0. 1101101*210
5.2 浮點數的乘除法
(1)階碼運算:階碼求和(乘法)或階碼求差(除法)
即 [Ex+Ey]移= [Ex]移+ [Ey]補
[Ex-Ey]移= [Ex]移+ [-Ey]補
(2)浮點數的尾數處理:浮點數中尾數乘除法運算結果要進行舍入處理
例題:X=0 .0110011*211,Y=0.1101101*2-10 求X*Y
解:[X]浮: 0 1 010 1100110
[Y]浮: 0 0 110 1101101
(1)階碼相加
[Ex+Ey]移=[Ex]移+[Ey]補=1 010+1 110=1 000
1 000為移碼錶示的0
(2)原碼尾數相乘的結果為:
0 10101101101110
(3)規格化處理:已滿足規格化要求,不需左規,尾數不變,階碼不變。
(4)舍入處理:按舍入規則,加1進行修正
所以 X※Y= 0.1010111*20
/******************************************************************************************
*【Author】:flyingbread
*【Date】:2007年3月2日
*【Notice】:
*1、本文為原創技術文章,首發部落格園個人網站(http://flyingbread.cnblogs.com/),轉載和引用請註明作者及出處。
*2、本文必須全文轉載和引用,任何組織和個人未授權不能修改任何內容,並且未授權不可用於商業。
*3、本聲明為文章一部分,轉載和引用必須包括在原文中。
*4、本文參考了網路上的若干資料,不一一列舉,但是一併致謝。