浮點數

編程語言

浮點數是屬於有理數中某特定子集的數的數字錶示,在計算機中用以近似表示任意某個實數。具體的說,這個實數由一個整數或定點數(即尾數)乘以某個基數(計算機中通常是2)的整數次冪得到,這種表示方法類似於基數為10的科學計數法。

浮點數


浮點計算

是指浮點數參與的運算,這種運算通常伴隨著因為無法精確表示而進行的近似或舍入。
一個浮點數a由兩個數m和e來表示:a = m × b^e。在任意一個這樣的系統中,我們選擇一個基數b(記數系統的基)和精度p(即使用多少位來存儲)。m(即尾數)是形如±d.ddd...ddd的p位數(每一位是一個介於0到b-1之間的整數,包括0和b-1)。如果m的第一位是非0整數,m稱作規格化的。有一些描述使用一個單獨的符號位(s 代表+或者-)來表示正負,這樣m必須是正的。e是指數。

結構

由此可以看出,在計算機中表示一個浮點數,其結構如下:
尾數部分(定點小數)階碼部分(定點整數)
階符±階碼e數符±尾數m
這種設計可以在某個固定長度的存儲空間內表示定點數無法表示的更大範圍的數。
浮點加法減法運算
設有兩個浮點數x和y,它們分別為
x = Mx*2^Ex
y = My*2^Ey
其中Ex和Ey分別為數x和y的階碼,Mx和My為數x和y的尾數。
兩浮點數進行加法和減法的運算規則是
設 Ex小於等於Ey,則 x±y = (Mx*2^(Ex-Ey)±My)*2^Ey,
完成浮點加減運算的操作過程大體分為四步:
1. 0 操作數的檢查;
2. 比較階碼大小並完成對階;
3. 尾數進行加或減運算;
4. 結果規格化並進行舍入處理。
⑴ 0 操作數檢查
浮點加減運算過程比定點運算過程複雜。如果判知兩個操作數x或y中有一個數為0,即可得知運算結果而沒有必要再進行後續的一系列操作以節省運算時間。0操作數檢查步驟則用來完成這一功能。
⑵ 比較階碼大小並完成對階
兩浮點數進行加減,首先要看兩數的階碼是否相同,即小數點位置是否對齊。若二數階碼相同,表示小數點是對齊的,就可以進行尾數的加減運算。反之,若二數階碼不同,表示小數點位置沒有對齊,此時必須使二數階碼相同,這個過程叫作對階。
要對階,首先應求出兩數階碼Ex和Ey之差,即
△E = Ex-Ey
若△E=0,表示兩數階碼相等,即Ex=Ey;若△E>0,表示Ex>Ey;若△E<0,表示Ex
當Ex≠Ey 時,要通過尾數的移動以改變Ex或Ey,使之相等。原則上,既可以通過Mx移位以改變Ex來達到Ex=Ey,也可以通過My移位以改變Ey來實現Ex=Ey。但是,由於浮點表示的數多是規格化的,尾數左移會引起最高有效位的丟失,造成很大誤差。尾數右移雖引起最低有效位的丟失,但造成誤差較小。因此,對階操作規定使尾數右移,尾數右移后階碼作相應增加,其數值保持不變。顯然,一個增加后的階碼與另一個階碼相等,增加的階碼的一定是小階。因此在對階時,總是使小階向大階看齊,即小階的尾數向右移位(相當於小數點左移)每右移一位,其階碼加1,直到兩數的階碼相等為止,右移的位數等於階差△E。
⑶ 尾數求和運算
對階結束后,即可進行尾數的求和運算。不論加法運算還是減法運算,都按加法進行操作,其方法與定點加減法運算完全一樣。
⑷ 結果規格化
在浮點加減運算時,尾數求和的結果也可以得到01.ф…ф或10.ф…ф,即兩符號位不等,這在定點加減法運算中稱為溢出,是不允許的。但在浮點運算中,它表明尾數求和結果的絕對值大於1,向左破壞了規格化。此時將運算結果右移以實現規格化表示,稱為向右規格化。規則是:尾數右移1位,階碼加1。當尾數不是1.M時需向左規格化。
⑸ 舍入處理
在對階或向右規格化時,尾數要向右移位,這樣,被右移的尾數的低位部分會被丟掉,從而造成一定誤差,因此要進行舍入處理。
簡單的舍入方法有兩種:一種是"0舍1入"法,即如果右移時被丟掉數位的最高位為0則捨去,為1則將尾數的末位加"1"。另一種是"恆置一"法,即只要數位被移掉,就在尾數的末尾恆置"1"。
在IEEE754標準中,舍入處理提供了四種可選方法:
就近舍入其實質就是通常所說的"四捨五入"。例如,尾數超出規定的23位的多餘位數字是10010,多餘位的值超過規定的最低有效位值的一半,故最低有效位應增1。若多餘的5位 是01111,則簡單的截尾即可。對多餘的5位10000這種特殊情況:若最低有效位現為0,則截 尾;若最低有效位現為1,則向上進一位使其變為 0。
朝0舍入 即朝數軸原點方向舍入,就是簡單的截尾。無論尾數是正數還是負數,截尾都使取值的絕對值比原值的絕對值小。這種方法容易導致誤差積累。
朝+∞舍入 對正數來說,只要多餘位不全為0則向最低有效位進1;對負數來說則是簡單的截尾。
朝-∞舍入 處理方法正好與 朝+∞舍入情況相反。對正數來說,只要多餘位不全為0則簡單截尾;對負數來說,向最低有效位進1。
⑹ 溢出處理
浮點數的溢出是以其階碼溢出表現出來的。在加\減運算過程中要檢查是否產生了溢出:若階碼正常,加(減)運算正常結束;若階碼溢出,則要進行相應處理。另外對尾數的溢出也需要處理。
階碼上溢 超過了階碼可能表示的最大值的正指數值,一般將其認為是+∞和-∞。
階碼下溢 超過了階碼可能表示的最小值的負指數值,一般將其認為是0。
尾數上溢 兩個同符號尾數相加產生了最高位向上的進位,將尾數右移,階碼增1來重新對齊。
尾數下溢 在將尾數右移時,尾數的最低有效位從尾數域右端流出,要進行舍入處理。

實例


題目

例如,一個指數範圍為±4的4位十進位浮點數可以用來表示43210,4.321或0.0004321,但是沒有足夠的精度來表示432.123和43212.3(必須近似為432.1和43210)。當然,實際使用的位數通常遠大於4。

特別數值

此外,浮點數表示法通常還包括一些特別的數值:+∞和−∞(正負無窮大)以及NaN('Not a Number')。無窮大用於數太大而無法表示的時候,NaN則指示非法操作或者無法定義的結果。

二進位表示

眾所周知,計算機中的所有數據都是以二進位表示的,浮點數也不例外。然而浮點數的二進位表示法卻不像定點數那麼簡單了。

概念

先澄清一個概念,浮點數並不一定等於小數,定點數也並不一定就是整數。所謂浮點數就是小數點在邏輯上是不固定的,而定點數只能表示小數點固定的數值,具用浮點數或定點數表示某哪一種數要看用戶賦予了這個數的意義是什麼。
C++中的浮點數有6種,分別是:
float:單精度,32位
unsigned float:單精度無符號,32位
double:雙精度,64位
long double:高雙精度,80位
然而不同的編譯器對它們的支持也略有不同,據我所知,很多編譯器都沒有按照IEEE規定的標準80位支持后兩種浮點數的,大多數編譯器將它們視為double,或許還有極個別的編譯器將它們視為128位?!對於128位的long double我也僅是聽說過,沒有求證,哪位高人知道這一細節煩勞告知。
下面我僅以float(帶符號,單精度,32位)類型的浮點數說明C++中的浮點數是如何在內存中表示的。先講一下基礎知識,純小數的二進位表示。(純小數就是沒有整數部分的小數,講給小學沒好好學的人)
純小數要想用二進位表示,必須先進行規格化,即化為 1.xxxxx * ( 2 ^ n ) 的形式(“^”代表乘方,2 ^ n表示2的n次方)。對於一個純小數D,求n的公式如下:
n = 1 + log2(D); // 純小數求得的n必為負數
再用 D / ( 2 ^ n ) 就可以得到規格化后的小數了。接下來就是十進位到二進位的轉化問題,為了更好的理解,先來看一下10進位的純小數是怎麼表示的,假設有純小數D,它小數點后的每一位數字按順序形成一個數列:
{k1,k2,k3,...,kn}
那麼D又可以這樣表示:
D = k1 / (10 ^ 1 ) + k2 / (10 ^ 2 ) + k3 / (10 ^ 3 ) + ... + kn / (10 ^ n )
推廣到二進位中,純小數的表示法即為:
D = b1 / (2 ^ 1 ) + b2 / (2 ^ 2 ) + b3 / (2 ^ 3 ) + ... + bn / (2 ^ n )
現在問題就是怎樣求得b1,b2,b3,……,bn。演演算法描述起來比較複雜,還是用數字來說話吧。聲明一下,1 / ( 2 ^ n )這個數比較特殊,我稱之為位階值。

例二

例如0.456,第1位,0.456小於位階值0.5故為0;第2位,0.456大於位階值0.25,該位為1,並將0.456減去0.25得0.206進下一位;第3位,0.206大於位階值0.125,該位為1,並將0.206減去0.125得0.081進下一位;第4位,0.081大於0.0625,為1,並將0.081減去0.0625得0.0185進下一位;第5位0.0185小於0.03125……
最後把計算得到的足夠多的1和0按位順序組合起來,就得到了一個比較精確的用二進位表示的純小數了,同時精度問題也就由此產生,許多數都是無法在有限的n內完全精確的表示出來的,我們只能利用更大的n值來更精確的表示這個數,這就是為什麼在許多領域,程序員都更喜歡用double而不是float。
float的內存結構,我用一個帶位域的結構體描述如下:
struct MYFLOAT
{
bool bSign : 1; // 符號,表示正負,1位
char cExponent : 8; // 指數,8位
unsigned long ulMantissa : 32; // 尾數,32位
};
符號就不用多說了,1表示負,0表示正
指數是以2為底的,範圍是 -128 到 127,實際數據中的指數是原始指數加上127得到的,如果超過了127,則從-128開始計,其行為和X86架構的CPU處理加減法的溢出是一樣的。
比如:127 + 2 = -127;-127 - 2 = 127
尾數都省去了第1位的1,所以在還原時要先在第一位加上1。它可能包含整數和純小數兩部分,也可能只包含其中一部分,視數字大小而定。對於帶有整數部分的浮點數,其整數的表示法有兩種,當整數大於十進位的16777215時使用的是科學計數法,如果小於或等於則直接採用一般的二進位表示法。科學計數法和小數的表示法是一樣的。
小數部分則是直接使用科學計數法,但形式不是X * ( 10 ^ n ),而是X * ( 2 ^ n )。拆開來看。
0 000000000000000000000000000000
符號位 指數位 尾數位
--------------------------------------------------------------------------------

例三

判斷兩個浮點數是否相等。
在這個例子中我們以C++代碼來判別兩個浮點數是否相等。由於浮點數在存儲中無法精確表示,所以 fp1==fp2 無法準確的判斷float型變數fp1與fp2是否相等。應該使用(fp1-fl2)<0.0000001 來進行判斷。
示例:
bool equal(float fp1,float fp2)
{
if( abs( fp1 - fp2 ) < 0.00000001 ) return true;
else
return false;
}
--------------------------------------------------------------------------------

導數字分佈


簡介

作者: concreteHAM
什麼是浮點數,不用我多說,這裡我們要討論的是規格化的任意進位浮點數的前導數字的概率分佈
在《計算機程序設計藝術》第二卷中做了非常深入的討論,這裡我從中精鍊出要點。

實例

例如:
浮點數
浮點數
2.345 E 67這是一個十進位規格化浮點數,前導數字就是2。
就只有一個“隨機”的浮點數而言,討論其分散式沒有意義的,我們要討論的是充分多個“隨機”數進行的一系列運算后產生的浮點結果的前導數字分佈。
假設現在有一巨大的浮點數集,依此對數集中每個浮點數都乘以2,其中有一個十進位浮點數F,它的前導數字是1,那麼它底數可能的值範圍就是1.000…~1.999…,乘以一個數字2,那麼它的底數就變成2.000…~3.999…,很明顯乘以2以前前導數字是1的浮點個數與現在前導數字是2、3的浮點個數相同。以此我們接下來分析。
對於一個b進位的浮點數,它的前導數字x範圍就是0 < x < b,設f(x)是上述數集的前導數字的概率密度函數(註:是密度函數),那麼它在前導數字u和v之間(0
∫[u,v]f(x)dx ⑴
由前面所述的,對於一個充分小增量Δx,f(x)必須滿足這樣一個公式:
f⑴Δx = x*f(x)Δx ⑵
因為:
f⑴Δx是f⑴微分段內的概率,根據前面所述,f⑴Δx概率等於f(1*x)*(x*Δx)
很明顯:
f(x) = f⑴/x ⑶
兩邊在[1,b]之間進行積分,等號左邊必定為1,右邊等於f⑴ln(b):
1 = f⑴ln(b) ⑷
得:f⑴ = 1/ln(b) 帶入⑶中:
f(x) = 1/(x*ln(b))
那麼利用⑴式得:
∫[u,v]1/(x*ln(b))dx
= ln(v/u) / ln(b) ⑸
這就是求前導數字的概率分佈函數。
例如b = 10進位時,前導數字為1的概率就是:
= ln((1+1)/1) / ln⑽
≈ 0.301
前導數字為9的概率就是:
= ln((9+1)/9) / ln⑽
≈0.0458
以下是一個測試程序(Mathematica軟體):
T[n_,b_]:=Block[{res={},ran,i,a},
For[i=1,i
res=Append[res,0]
];
For[i=0,i
ran=Random[]*Random[]*Random[]; 充分打亂浮點數
ran=Log[b,ran];
a=Floor[b^(ran-Floor[ran])]; 取出前導數字
res[[a]]++ 對前導數字個數統計
];
Return[res]
]
執行T[100000,10],以10進位測試100000個浮點數,得到一個分佈:
{30149,18821,13317,9674,7688,6256,5306,4655,4134}
和理論值相當接近。
  • 目錄