一個浮點數計算的問題

twoon發表於2014-03-17

同事在工作中遇到了一個與浮點數運算相關的奇怪問題,值得一記,具體程式碼摘要如下:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 int main()
 5 {
 6     double s = 6.0;
 7     double e = 0.2;
 8 
 9     cout << static_cast<int>(s/e) << endl;
10     return 0;
11 }

這段程式碼看起來很簡單,心一算,應該輸出30才對。

但結果卻是我們在32 和 64位 linux平臺下得到了不同的結果,分別是29和30,意想不到吧?

然後,如果把程式碼改成如下:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 int main()
 5 {
 6     double s = 6.0;
 7     double e = 0.2;
 8 
 9     double d = s/e;
10 
11     cout << static_cast<int>(d) << endl;
12     return 0;
13 }

你會發現在兩個平臺上都得到了相同的“正確”結果!為什麼呢?

稀疏的浮點數

眾所周知,計算機是無法精確地表示所有浮點數的,無理數的稠密性使得無論我們用多高精度的資料型別來表示浮點數,所能表示的範圍相對整個無理數來說都是相當相當地稀疏。

因此在計算機的世界裡,我們只能儘可能地用有限精度來表示一定範圍的資料,至於那些沒法精確表示的數字,就只能在計算機所能表示的範圍裡找一個和它最接近的數來湊和湊和。

這個好像比較好理解,比如說根號2什麼的,我們都知道這些無理數不能在計算機裡完全精確地表示,但還有那麼一些有理數,在十進位制裡雖然可以精確地表示,在二進位制裡卻也是無法精確表示的,比如說上面例子中的0.2,你如果對此有懷疑,可以好好回顧一下怎麼把小數轉成二進位制,然後慢慢用筆在紙上演算一下。

講這些,無非還是想說明,計算機世界裡的浮點數是相當疏鬆地,借用《深入理解作業系統》一書裡的一張圖,讓大家感受一下:

 

上圖展示的是按照IEEE754標準,用一個6bit來表示浮點數時,所能表示的資料範圍。

浮點數的折斷與轉換

因為很多小數是無法精確表示的,因此我們只能儘可能在有限精度的小數裡找到最接近的數來近似那些無限的小數。

那麼計算機是怎麼樣來做這些逼近的呢?常用的有如下4種方式:

其中第一種是預設使用方式,需要注意的是這些折斷方式並不僅限於由浮點數轉為整數,浮點數之間也是適用的。

在C語言中,浮點數與整數的轉換有以下幾條原則:

1) int型轉為float,不會overfloat,但有些數用float無法表示,因此可能需要rounding,記住float是很稀疏的。

2) 由int或float轉為double時,精度不會丟失,畢竟double精度高太多了。

3) double轉為float時,很可能會overfloat, 轉換則用round-to-even的方式(預設)進行。

4) 由float, double轉換為int時用round-to-zero的方式轉換,當然也很可能會被截斷。

 

請注意第3條,第4條原則,它們轉換時使用的不同原則有時會導致一些很微妙的結果。

Intel IA32 浮點運算

IA32處理器和很多其它一些處理器一樣,有專門用於儲存浮點數的暫存器,當在cpu中進行浮點數運算時,這些暫存器就用來儲存輸入輸出及相關的中間結果。

但IA32有一個比較特別的地方,它的浮點數暫存器是80位的,而我們在程式中只用到32和64位兩種型別,因此當把float,double放入到cpu中時,它們都會先被轉換成了80位,然後以80位的方式進行運算,最後得到的結果再轉換回來。這樣特性使得浮點數的計算可以相對更精確些,但同時,一不小心很可能也會引出一些意想不到的問題。

你可能突然恍然大悟了,對的,我們最開始提到那個奇怪的問題就與此相關。

s/e得到結果是個80位的浮點數,由這個浮點數先轉換成double再轉成int,與直接就轉換成int,結果很可能是不同的。

比如在我們的例子中,s/e ~ 29.999999....時,s/e轉換成double使用round-to-even的方式,會得到也許是30.0000001,再轉成整形時,得到30.

但如果直接由29.99999...轉換成整型,得到卻是29。

 

後來新出的系列Intel處理器,包括IA32及64位的處理器,提供了專門的硬體來直接處理浮點數,使得可以分開對待float型與double型,這些硬體特性在compiler的支援下,可以生相對高效的程式碼,同時也避免了我們上面所遇到的問題,有興趣的讀者可以google一下相關的關鍵字:sse。

 

 

相關文章