2011-09-22 155 views
98

下面的代碼可以在Visual Studio 2008中使用和不使用優化。但它只適用於沒有優化的g ++(O0)。啓用優化的不同浮點結果 - 編譯器錯誤?

#include <cstdlib> 
#include <iostream> 
#include <cmath> 

double round(double v, double digit) 
{ 
    double pow = std::pow(10.0, digit); 
    double t = v * pow; 
    //std::cout << "t:" << t << std::endl; 
    double r = std::floor(t + 0.5); 
    //std::cout << "r:" << r << std::endl; 
    return r/pow; 
} 

int main(int argc, char *argv[]) 
{ 
    std::cout << round(4.45, 1) << std::endl; 
    std::cout << round(4.55, 1) << std::endl; 
} 

輸出應該是:

4.5 
4.6 

但G ++與優化(O1 - O3)將輸出:

4.5 
4.5 

如果餘噸前添加volatile關鍵字,它的工作原理,那麼可能會出現某種優化錯誤?

在g ++ 4.1.2和4.4.4上進行測試。

這裏是ideone結果: http://ideone.com/Rz937

而且我對G試驗期權++很簡單:

g++ -O2 round.cpp 

更有趣的結果是,即使我打開/fp:fast選項在Visual Studio 2008中,結果仍然是正確的。

進一步的問題:

我想知道,我就應該總是打開-ffloat-store選項?

因爲我測試的g ++版本是CentOS/Red Hat Linux 5和CentOS/Redhat 6附帶的

我在這些平臺下編譯了很多我的程序,我擔心它會在我的程序中引起意外的錯誤。調查我所有的C++代碼和使用庫是否有這樣的問題似乎有點困難。任何建議?

有人有興趣爲什麼即使/fp:fast打開,Visual Studio 2008仍然有效嗎?它看起來像視覺 工作室  2008在這個問題比g ++更可靠嗎?

+49

對於所有新的SO用戶:這是您如何提出問題。 +1 – tenfour

+1

FWIW,我用g ++ 4.5.0使用MinGW得到正確的輸出。 –

+0

我得到4.5 4.6的所有情況。你的g ++版本是什麼?我有g ++(Debian 4.3.2-1.1)4.3.2 –

回答

79

Intel x86處理器在內部使用80位擴展精度,而double通常爲64位寬。不同的優化級別會影響CPU的浮點值被保存到內存中的頻率,因此會從80位精度舍入到64位精度。

使用-ffloat-store gcc選項可獲得具有不同優化級別的相同浮點結果。

或者,使用long double類型,它通常是80位寬的gcc上,以避免從80位的舍入到64位精度。

man gcc說,這一切:

-ffloat-store 
     Do not store floating point variables in registers, and inhibit 
     other options that might change whether a floating point value is 
     taken from a register or memory. 

     This option prevents undesirable excess precision on machines such 
     as the 68000 where the floating registers (of the 68881) keep more 
     precision than a "double" is supposed to have. Similarly for the 
     x86 architecture. For most programs, the excess precision does 
     only good, but a few programs rely on the precise definition of 
     IEEE floating point. Use -ffloat-store for such programs, after 
     modifying them to store all pertinent intermediate computations 
     into variables. 
+20

我認爲這是答案。常數4.55被轉換爲4.54999999999999,這是64位中最接近的二進制表示;乘以10再循環到64位,你得到45.5。如果通過將其保留在80位寄存器中而忽略舍入步驟,則最終將獲得45.4999999999999。 –

+0

謝謝,我甚至不知道這個選項。但我想知道,我應該總是打開-ffloat-store選項嗎?因爲我測試過的g ++版本隨CentOS/Redhat 5和CentOS/Redhat 6一起提供。我在這些平臺下編譯了許多我的程序,我擔心這會在我的程序中引發意外的錯誤。 – Bear

+0

@Mark:但是如何解釋調試語句:std :: cout <<「t:」<< t << std :: endl;輸出將是正確的? – Bear

6

不同的編譯器有不同的優化設置。根據IEEE 754,其中一些更快的優化設置不會保持嚴格的浮點規則。 Visual Studio有一個特定的設置,/fp:strict,/fp:precise,/fp:fast,其中/fp:fast違反了可以做什麼的標準。您可能會發現這個標誌是控制這些設置中的優化的標誌。你也可以在GCC中找到一個類似的設置來改變行爲。

如果是這種情況,那麼編譯器之間唯一不同的是GCC會在更高優化級別上默認尋找最快的浮點行爲,而Visual Studio不會更改具有更高優化級別的浮點行爲。因此,它可能不一定是一個實際的錯誤,但是一個你不知道你正在開啓的選項的預期行爲。

+4

對於GCC,有一個'-ffast-math'開關,並且它沒有被自引用的任何'-O'優化級別開啓:「它可能導致 程序的錯誤輸出,這取決於IEEE的確切實現或ISO規則/數學規範 函數「。 – Mat

+0

@Mat:我已經嘗試過'-fast-math'和一些其他的東西在我的'g ++ 4.4.3'上,我仍然無法重現這個問題。 – NPE

+0

不錯:用'-ffast-math'我在兩種情況下都得到了'4.5',優化級別大於'0'。 –

3

對於那些誰也無法重現錯誤:不取消對註釋掉調試支桿,它們影響的結果。

這意味着該問題與調試語句。它看起來像有造成在輸出語句把值裝載到寄存器的舍入誤差,這就是爲什麼其他人發現,你可以用-ffloat-store

進一步的問題解決此問題:

我在想,應該我總是打開-ffloat-store選項?

要輕率,必須有一個原因,有些程序員不打開-ffloat-store,否則選項將不存在(同樣,必須有一個原因,一些程序員打開-ffloat-store) 。我不建議總是打開它或關閉它。打開它可以防止某些優化,但關閉它可以實現您獲得的那種行爲。

但是,通常在二進制浮點數(如計算機使用)和十進制浮點數(人們熟悉的)之間有some mismatch,並且這種不匹配會導致類似的行爲, ,你得到的行爲是不造成此不匹配,但類似行爲可以是)。問題是,既然你在處理浮點時已經有些模糊,我不能說-ffloat-store會使它更好或更糟。

相反,您可能想要查看other solutions以解決您正在嘗試解決的問題(不幸的是,Koenig並未指向實際的論文,而且我也找不到明顯的「規範」地點,所以我必須寄給你Google)。


如果你不進行輸出的目的四捨五入,我可能會看std::modf()(在cmath)和std::numeric_limits<double>::epsilon()(在limits)。在原始round()功能的思考,我相信這將是清潔的號召與該函數的調用替換到std::floor(d + .5)

// this still has the same problems as the original rounding function 
int round_up(double d) 
{ 
    // return value will be coerced to int, and truncated as expected 
    // you can then assign the int to a double, if desired 
    return d + 0.5; 
} 

我想提出以下建議改進:

// this won't work for negative d ... 
// this may still round some numbers up when they should be rounded down 
int round_up(double d) 
{ 
    double floor; 
    d = std::modf(d, &floor); 
    return floor + (d + .5 + std::numeric_limits<double>::epsilon()); 
} 

簡單注意:std::numeric_limits<T>::epsilon()被定義爲「添加到1的最小數字,它創建的數字不等於1」。您通常需要使用相對的epsilon(即,以某種方式計算epsilon的比例來說明您使用的是除「1」之外的數字)。 d,.5std::numeric_limits<double>::epsilon()的總和應該接近1,因此將該添加進行分組意味着std::numeric_limits<double>::epsilon()對於我們正在做的事情來說大小合適。如果有的話,std::numeric_limits<double>::epsilon()將會過大(當三者的總和小於1時),並且可能導致我們在不應該的時候將一些數字加起來。


現在,你應該考慮std::nearbyint()

+0

「相對epsilon」被稱爲1 ulp(最後1個單位)。 'x - nextafter(x,INFINITY)'與x的1個ulp相關(但不要使用它;我確定存在轉角的情況,我只是做了這個)。 'epsilon()'的cppreference示例[有一個縮放它以獲得基於ULP的相對錯誤的示例](http://en.cppreference.com/w/cpp/types/numeric_limits/epsilon)。 –

+2

順便說一句,'-ffloat-store'的2016回答是:首先不要使用x87。因爲SSE ​​/ SSE2具有臨時性而沒有額外的精度,所以使用SSE2數學(64位二進制文​​件或'-mfpmath = sse -msse2'來製作硬的舊的32位二進制文​​件)。 XMM寄存器中的「double」和「float」變量實際上是IEEE 64位或32位格式。 (與x87不同的是,寄存器始終爲80位,並將存儲器循環存儲到32位或64位。) –

10

輸出應爲:4.5 4.6 這就是輸出會是什麼,如果你有無限的精度,或者如果你用的是使用十進制爲基礎的,而不是二進制爲基礎的浮點表示設備工作。但是,你不是。大多數計算機使用二進制IEEE浮點標準。

由於馬克西姆Yegorushkin已經注意到了他的答案,部分問題的是在內部計算機使用一個80位浮點表示。但這只是問題的一部分。問題的基礎是,任何形式的n.nn5都沒有確切的二進制浮點表示。那些角落案件總是不準確的數字。

如果您確實希望您的舍入能夠可靠地舍入這些角落案例,則需要舍入算法來解決n.n5,n.nn5或n.nnn5等(但不是n。 5)總是不準確的。查找確定某個輸入值是否向上或向下取整的角落案例,並根據與此角落案例的比較返回向上或向下取整的值。而且您確實需要注意,優化編譯器不會將所找到的角落案例放入擴展的精確寄存器中。

對於這樣的算法,參見How does Excel successfully Rounds Floating numbers even though they are imprecise?

或者你也可以接受這樣一個事實,即角落案件有時會錯誤地發生。

0

就我個人而言,我遇到了相同的問題 - 從gcc到VS.在大多數情況下,我認爲最好避免優化。唯一值得的是當你處理涉及大數據浮點數據的數值方法時。即使在反彙編之後,我經常不知道編譯器的選擇。通常使用編譯器內部函數更容易,或者只是自己編寫程序集。