web-dev-qa-db-ja.com

最適化を有効にした異なる浮動小数点の結果-コンパイラのバグ?

以下のコードは、最適化の有無にかかわらず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キーワードをtの前に追加すると機能します。そのため、何らかの最適化のバグがあるかもしれません。

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は引き続き動作しますか? Visual Studio 2008はg ++よりもこの問題の信頼性が高いようです。

102
Bear

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

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

または、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.
86

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

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

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

このようなアルゴリズムについては、 Excelは浮動小数点数が不正確であっても正常に丸める方法 を参照してください。

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

10
David Hammen

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

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

6
Puppy

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

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

さらなる質問:

私は、_-ffloat-store_オプションを常にオンにすべきかと思っていましたか?

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

しかし、一般的に、(コンピューターが使用するような)2進浮動小数点数と(人々がよく知っている)10進浮動小数点数との間には 不一致 があり、その不一致は取得するものと同様の動作を引き起こす可能性があります(明確にするために、あなたが得ている振る舞いは、この不一致によって引き起こされるnotですが、similar動作canbe)。問題は、浮動小数点を扱うときにすでに曖昧さを持っているので、_-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() を考慮する必要があります。

4
Max Lybbert

この問題をさらに掘り下げて、精度を上げることができます。まず、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

Maxim 上記のように、問題はFPUレジスタの80ビットサイズによるものです。

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

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]);
}

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

X86_64アーキテクチャでは、 tmandry で述べたようにSSEが使用されるため、-mfpmath=387を使用してFPコンピューティングに古いx87 FPUを強制しない限り、問題は発生しません。または、-m32を使用して32ビットモードでコンパイルしない限り(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

1
calandoa

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

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

これを分解しましょう。

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

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

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

FMAには1つの丸めのみがあり(内部の一時的な乗算結果に対して事実上無限の精度を維持します)、ADD + MULには2つの丸めがあります。

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

1
tmandry

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

0
cdcdcd