最適化が有効になっていると異なる浮動小数点結果-コンパイラのバグ?


109

以下のコードは、最適化の有無にかかわらず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

volatiletの前にキーワードを追加すると機能するので、何らかの最適化のバグがあるのでしょうか?

g ++ 4.1.2、および4.4.4でテストします。

ここideone上の結果は次のとおりです。 http://ideone.com/Rz937

そして、私がg ++でテストするオプションは簡単です:

g++ -O2 round.cpp

より興味深い結果、/fp:fastVisual Studio 2008でオプションをオンにしても、結果は正しいです。

さらに質問:

いつも-ffloat-storeオプションをオンにする必要があるのでしょうか。

私がテストしたg ++バージョンはCentOS / Red Hat Linux 5およびCentOS / Redhat 6に同梱されているためです。

これらのプラットフォームでプログラムの多くをコンパイルしましたが、プログラム内で予期しないバグが発生するのではないかと心配しています。C ++のすべてのコードと使用されているライブラリーを、そのような問題があるかどうかを調査するのは少し難しいようです。なにか提案を?

/fp:fastオンにしても、Visual Studio 2008がまだ機能する理由に関心がある人はいますか?Visual Studio 2008はこの問題でg ++よりも信頼性が高いようです。


51
すべての新しいSOユーザーへ:これはあなたが質問する方法です。+1
テンフォア2011

1
FWIW、MinGWを使用してg ++ 4.5.0で正しい出力を取得しています。
スティーブブラックウェル


5
あなたのルーチンがあらゆる種類の出力で確実に機能する可能性は低いことを覚えておいてください。doubleを整数に丸めるのとは対照的に、これはすべての実数が表現できるわけではないので、このようなバグがさらに発生することが予想されるという事実に対して脆弱です。
Jakub Wieczorek

2
バグを再現できない場合は、コメントアウトされたデバッグステートメントのコメントを解除しないでください。結果に影響します。
n。「代名詞」m。

回答:


91

Intel x86プロセッサは内部で80ビット拡張精度を使用しますdoubleが、通常は64ビット幅です。異なる最適化レベルは、CPUからの浮動小数点値がメモリに保存される頻度に影響し、80ビット精度から64ビット精度に丸められます。

-ffloat-storegccオプションを使用して、異なる最適化レベルで同じ浮動小数点の結果を取得します。

または、long doubleタイプを使用します。これは通常、80ビットから64ビットの精度への丸めを回避するために、gccでは80ビット幅です。

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.

x86_64版ではコンパイラがためにSSEレジスタを使用し構築するfloatと、double何の拡張精度が使用されていないと、この問題は発生しないように、デフォルトで。

gccコンパイラオプション-mfpmathはそれを制御します。


20
これが答えだと思います。定数4.55は、64ビットで最も近いバイナリ表現である4.54999999999999に変換されます。10を掛けて64ビットに再度丸めると、45.5になります。80ビットのレジスタに保持して丸めステップをスキップすると、45.4999999999999になります。
Mark Ransom

おかげで、私もこのオプションを知りません。しかし、私は疑問に思っていました、常に-ffloat-storeオプションをオンにすべきですか?テストしたg ++バージョンにはCentOS / Redhat 5およびCentOS / Redhat 6が付属しているため、これらのプラットフォームで多くのプログラムをコンパイルしたため、プログラム内で予期しないバグが発生するのではないかと心配しています。
クマ

5
@Bear、デバッグ文により、値がレジスタからメモリにフラッシュされる可能性があります。
Mark Ransom

2
@ Bear、64ビットfloatがアンダーフローまたはオーバーフローして生成されることが予想されるときに極端に小さい値または巨大な値で動作しない限り、通常、アプリケーションは拡張精度の恩恵を受けるはずinfです。良い経験則はありません、ユニットテストはあなたに明確な答えを与えることができます。
Maxim Egorushkin、2011

2
@bear原則として、完全に予測可能な結果が必要な場合、および/または人間が紙面で合計を計算する場合とまったく同じ結果が必要な場合は、浮動小数点を使用しないでください。-ffloat-storeは、予測不能の原因を1つ取り除きますが、特効薬ではありません。
プラグウォッシュ2015年

10

出力は次のようになります。4.54.6無限の精度がある場合、または2進数ベースではなく10進数ベースの浮動小数点表現を使用するデバイスで作業している場合、これが出力になります。しかし、そうではありません。ほとんどのコンピューターは、バイナリIEEE浮動小数点標準を使用します。

Maxim Yegorushkinが彼の回答ですでに述べたように、問題の一部は内部的にコンピューターが80ビット浮動小数点表現を使用していることです。ただし、これは問題の一部にすぎません。この問題の根本は、n.nn5形式の任意の数が正確なバイナリ浮動表現を持たないことです。それらのコーナーケースは常に不正確な数値です。

これらのコーナーケースを確実に丸めることが本当に必要な場合は、n.n5、n.nn5、またはn.nnn5など(ただしn.5ではない)が常にであるという事実に対処する丸めアルゴリズムが必要です。不正確。一部の入力値が切り上げられるか切り捨てられるかを決定するコーナーケースを見つけ、このコーナーケースとの比較に基づいて、切り上げまたは切り捨てられた値を返します。また、最適化コンパイラが、見つかったコーナーケースを拡張精度レジスタに入れないように注意する必要があります。

Excelが浮動小数点数が不正確な場合でも、浮動小数点数を正しく丸める方法を参照してくださいそのようなアルゴリズムのために。

または、コーナーケースが誤って丸められることがあるという事実に対処することもできます。


6

コンパイラごとに最適化設定は異なります。これらのより高速な最適化設定の一部は、IEEE 754に従って厳密な浮動小数点ルールを維持しません。Visual Studioは、特定の設定が、持っている/fp:strict/fp:precise/fp:fast、どこ/fp:fastに何ができるか上の標準に違反します。このフラグがそのような設定での最適化を制御するものであることに気付くかもしれません。GCCにも同様の設定があり、動作が変わります。

これが事実である場合、コンパイラ間で異なる唯一のことは、GCCがデフォルトでより高い最適化で最速の浮動小数点動作を探すのに対し、Visual Studioはより高い最適化レベルで浮動小数点動作を変更しないことです。したがって、それは必ずしも実際のバグではないかもしれませんが、オンにしたことを知らなかったオプションの意図された動作です。


4
-ffast-mathGCCにはスイッチがあり -O、引用以降、どの最適化レベルでもオンにされていません。「数学関数のIEEEまたはISOルール/仕様の正確な実装に依存するプログラムの出力が誤っている可能性があります。」
Mat

@マット:私が試してみましたが-ffast-math、他のいくつかのことを試しましたが、g++ 4.4.3まだ問題を再現できません。
NPE

ニース:と-ffast-math私は入手できますか4.5より大きい最適化レベルの両方のケースで0
Kerrek SB、2011

(訂正:私が手4.5-O1して-O2ではなく、と、-O0-O3GCC 4.4.3ではなく、と-O1,2,3。GCC 4.6.1で)
Kerrek SB

4

バグを再現できない場合は、コメントアウトされたデバッグステートメントのコメントを外さないでください。結果に影響します。

これは、問題がデバッグ文に関連していることを意味します。そして、それは出力ステートメント中にレジスタに値をロードすることによって引き起こされた丸めエラーがあるように見えます、それが他の人があなたがこれでこれを修正できることがわかった理由です-ffloat-store

さらに質問:

いつも-ffloat-storeオプションをオンにする必要がありますか?

ひっくり返るには、一部のプログラマーがオンにならない理由がなければなりません-ffloat-store。そうでない場合、オプションは存在しません(同様に、一部のプログラマーオンにする理由があるはずです-ffloat-store)。常にオンまたはオフにすることはお勧めしません。オンにすると一部の最適化が妨げられますが、オフにすると、得られるような動作が可能になります。

しかし、一般的に、2進浮動小数点数(コンピューターが使用するような)と10進浮動小数点数(ユーザーがよく知っている)の間にはいくつかの不一致があり、その不一致により、取得したものと同様の動作が発生する可能性があります(明確に、動作この不一致が原因ではありませんが、同様の動作発生する可能があります)実のところ、浮動小数点を処理するときには既に曖昧さ-ffloat-storeがあるので、それが良くも悪くもなるとは言えません。

代わりに、解決しようとしている問題の他の解決策を検討することをお勧めします(残念ながら、ケーニッヒは実際の論文を指さず、そのための明確な「標準」の場所を実際に見つけることができないため、私は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に追加される最小の数」として定義されます。通常、相対イプシロンを使用する必要があります(つまり、 "1"以外の数値を処理しているという事実を考慮して、イプシロンを何らかの方法でスケールします)。合計d.5およびstd::numeric_limits<double>::epsilon()1近く、そのほかの手段でグループ化するようでなければなりませんstd::numeric_limits<double>::epsilon()私たちがやっていることのために適切なサイズ程度になりますが。どちらかとstd::numeric_limits<double>::epsilon()言えば、大きすぎ(3つすべての合計が1未満の場合)、必要でない場合に一部の数値を切り上げることがあります。


今日、あなたは考慮すべきstd::nearbyint()です。


「相対イプシロン」は1 ulp(最後の1ユニット)と呼ばれます。 x - nextafter(x, INFINITY)xの1 ulpに関連しています(ただし、それを使用しないでください。コーナーケースがあると確信しており、これを作成しました)。のcppreferenceの例にepsilon() は、ULPベースの相対エラーを取得するためにスケーリングする例があります
Peter Cordes、2016年

2
ところで、2016年の答え-ffloat-storeは次のとおりです。そもそもx87を使用しないでください。-mfpmath=sse -msse2SSE / SSE2には余分な精度のない一時変数があるため、SSE2数学(64ビットバイナリ、または無愛想な古い32ビットバイナリを作成するため)を使用します。 doubleそして、floatXMMレジスタのVARSは、IEEE 64ビットまたは32ビット形式で実際にあります。(x87とは異なり、レジスタは常に80ビットであり、メモリに32ビットまたは64ビットに丸められます。)
Peter Cordes

3

SSE2を含まないx86ターゲットにコンパイルする場合、受け入れられた答えは正しいです。最新のx86プロセッサーはすべてSSE2をサポートしているため、SSE2を利用できる場合は、次のことを行う必要があります。

-mfpmath=sse -msse2 -ffp-contract=off

これを分解してみましょう。

-mfpmath=sse -msse2。これは、SSE2レジスタを使用して丸めを実行します。これは、すべての中間結果をメモリに格納するよりもはるかに高速です。これはすでに x86-64のGCC のデフォルトであることに注意してください。GCC wikiから:

SSE2をサポートする最新のx86プロセッサでは、コンパイラオプション-mfpmath=sse -msse2を指定すると、すべての浮動小数点演算と倍精度演算がSSEレジスタで実行され、正しく丸められます。これらのオプションはABIに影響を与えないため、予測可能な数値結果を得るために可能な限り使用する必要があります。

-ffp-contract=off。ただし、丸めを制御するだけでは完全一致には不十分です。FMA(融合乗算加算)命令は、融合されていない対応するものと比較して丸め動作を変更する可能性があるため、無効にする必要があります。これは、GCCではなくClangのデフォルトです。この回答で説明されているように

FMAには丸めが1つしかありません(内部の一時的な乗算結果の無限精度を効果的に維持します)が、ADD + MULには2つあります。

FMAを無効にすると、パフォーマンスと精度を犠牲にして、デバッグとリリースで正確に一致する結果が得られます。SSEおよびAVXの他のパフォーマンス上の利点を引き続き利用できます。


1

私はこの問題をさらに掘り下げて、より高い精度をもたらすことができます。まず、x84_64のgccによる4.45および4.55の正確な表現は次のとおりです(最後の精度を出力するlibquadmathを使用)。

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

マキシムは上記述べ、問題は、FPUレジスタの80ビットの大きさによるものです。

しかし、Windowsで問題が発生しないのはなぜですか?IA-32では、x87 FPUは、仮数に53ビットの内部精度を使用するように構成されていました(64ビットの合計サイズに相当:) double。LinuxおよびMac OSの場合、デフォルトの精度である64ビットが使用されました(合計サイズ80ビットに相当:) long double。したがって、これらの異なるプラットフォームでは、FPUのコントロールワードを変更することで問題が発生する可能性があるかどうかは不明です(命令のシーケンスがバグをトリガーすると想定)。問題はバグ323としてgccに報告されました(少なくともコメント92を読んでください!)。

Windowsで仮数精度を表示するには、これをVC ++を使用して32ビットでコンパイルできます。

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

Linux / Cygwinの場合:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

-mpc32/64/80Cygwinでは無視されますが、gccではFPUの精度をで設定できることに注意してください。ただし、仮数のサイズは変更されますが指数は変更されず、他の種類の異なる動作への扉が開かれることに注意してください。

x86_64アーキテクチャーでは、tmandryが言うようにSSEが使用されるため、FPコンピューティング用に古いx87 FPUをで強制し-mfpmath=387ない限り、または32ビットモードでコンパイルしない限り-m32(multilibパッケージが必要)、問題は発生しません。Linuxでフラグとgccのバージョンのさまざまな組み合わせで問題を再現できます。

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

WindowsまたはCygwinでVC ++ / gcc / tccを使用していくつかの組み合わせを試しましたが、バグは表示されませんでした。生成された命令のシーケンスは同じではないと思います。

最後に、この問題を4.45または4.55で回避するためのエキゾチックな方法はを使用すること_Decimal32/64/128ですが、サポートは非​​常に少ないことに注意してください。printfをlibdfp


0

個人的には、gccからVSまで、同じ問題を他の方法で行っています。ほとんどの場合、最適化を回避する方が良いと思います。価値のある唯一の時間は、浮動小数点データの大きな配列を含む数値メソッドを扱う場合です。分解した後でも、コンパイラの選択にしばしば圧倒されます。多くの場合、コンパイラ組み込み関数を使用するか、自分でアセンブリを記述する方が簡単です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.