一個浮點數計算的問題


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

 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。

 

參考

1. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=35488

2. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=323

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM