4

昨日、私は自分のプロジェクトでバグを追跡していました.数時間後、多かれ少なかれ次のようなことをしているコードに絞り込みました:

#include <iostream>
#include <cmath>
#include <cassert>

volatile float r = -0.979541123;
volatile float alpha = 0.375402451;

int main()
{
    float sx = r * cosf(alpha); // -0.911326
    float sy = r * sinf(alpha); // -0.359146
    float ex = r * cosf(alpha); // -0.911326
    float ey = r * sinf(alpha); // -0.359146
    float mx = ex - sx;     // should be 0
    float my = ey - sy;     // should be 0
    float distance = sqrtf(mx * mx + my * my) * 57.2958f;   // should be 0, gives 1.34925e-06

//  std::cout << "sv: {" << sx << ", " << sy << "}" << std::endl;
//  std::cout << "ev: {" << ex << ", " << ey << "}" << std::endl;
//  std::cout << "mv: {" << mx << ", " << my << "}" << std::endl;
    std::cout << "distance: " << distance << std::endl;

    assert(distance == 0.f);
//  assert(sx == ex && sy == ey);
//  assert(mx == 0.f && my == 0.f);
} 

コンパイルと実行後:

$ g++ -Wall -Wextra -Wshadow -march=native -O2 vfma.cpp && ./a.out 
distance: 1.34925e-06
a.out: vfma.cpp:23: int main(): Assertion `distance == 0.f' failed.
Aborted (core dumped)

私の観点からは、2 つのビットごとに同一のペアの 2 つの減算を要求し (2 つのゼロを取得すると予想していました)、それらを 2 乗し (再び 2 つのゼロ)、それらを加算 (ゼロ) したため、何かが間違っています。

問題の根本的な原因は、融合乗算加算操作の使用であることが判明しました。これにより、どこかで結果が不正確になります(私の観点からは)。より正確な結果が得られると約束されているため、通常、この最適化に反対するものは何もありませんが、この場合、1.34925e-06 は、私が期待していた 0 とはかけ離れています。

テストケースは非常に「壊れやすい」です。より多くの出力またはより多くのアサートを有効にすると、コンパイラが fused-multiply-add を使用しなくなったため、アサートが停止します。たとえば、すべての行のコメントを外すと:

$ g++ -Wall -Wextra -Wshadow -march=native -O2 vfma.cpp && ./a.out 
sv: {-0.911326, -0.359146}
ev: {-0.911326, -0.359146}
mv: {0, 0}
distance: 0

これはコンパイラのバグだと思ったので報告しましたが、これは正しい動作だという説明で締めくくられました。

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

だから私は疑問に思っています-問題を回避するには、そのような計算をどのようにコーディングする必要がありますか? 私は一般的な解決策について考えていましたが、次のものよりも優れています。

mx = ex != sx ? ex - sx : 0.f;

とにかく、コードを修正または改善したいと思います-修正/改善するものがあれば--ffp-contract=offプロジェクト全体に設定するのではなく、とにかくコンパイラライブラリで内部的に fuse-multiply-add が使用されているため(sinf( ) と cosf()) であるため、解決策ではなく「部分的な回避策」になります...「浮動小数点を使用しない」(;

4

3 に答える 3

1

この質問はしばらく前から出回っていることがわかりますが、他の人が答えを探しているときに出くわした場合に備えて、いくつかの点について言及したいと思います..

まず、結果のアセンブリ コードを分析せずに正確に判断することは困難ですが、FMA が予想をはるかに超える結果を出す理由は、FMA 自体だけでなく、すべての計算が実行されていると想定しているためだと思います。指定した順序で実行されますが、C/C++ コンパイラを最適化すると、多くの場合、そうではありません。これはおそらく、print ステートメントのコメントを外すと結果が変わる理由でもあります。

コメントが示唆するようにmxmyが計算されていた場合、ファイナルmx*mx + my*myが FMA で行われたとしても、予想される 0 の結果が得られます。問題は、sx/ sy/ ex/ ey/ mx/変数のmyいずれも他の変数によって使用されていないため、コンパイラが実際にそれらを独立した変数としてまったく評価せず、すべての数学をまとめて大量の変数にする可能性が高いことです。乗算、加算、および減算をdistance1 つのステップで計算します。これは、マシン コードで任意の数の異なる方法で (任意の順序で、潜在的に複数の FMA などで) 表すことができますが、その 1 つに対して最高のパフォーマンスが得られると考えられます。大きな計算。

ただし、他の何か (print ステートメントなど) がmxおよびを参照する場合、コンパイラは、2 番目のステップとしてmy計算する前に、それらを個別に計算する可能性が高くなります。distanceその場合、数学はコメントが示唆する方法で機能し、最終distance計算の FMA でさえ結果を変更しません (入力がすべて正確に 0 であるため)。

答え

しかし、それは実際には本当の質問に答えていません。それに対する答えとして、一般的にこの種の問題を回避するための最も堅牢な (そして一般的に推奨される) 方法は次のとおりです。==浮動小数点数を比較するために使用するのは悪い考えです。代わりに、小さな数値 (イプシロンと呼ばれることが多い) を選択する必要があります。これは、起こりうる/累積された可能性のある誤差よりも大きく、重要な結果よりも小さい値です (たとえば、関心のある距離が実際にのみであることがわかっている場合)。小数点以下の桁数が重要な場合は、選択できますEPSILON = 0.01、これは「0.01 未満の差はゼロと同じと見なす」ことを意味します)。次に、言う代わりに:

assert(distance == 0.f);

あなたは言うでしょう:

assert(distance < EPSILON);

(あなたのイプシロンの正確な値はアプリケーションに依存する可能性が高く、もちろん計算の種類によって異なる場合もあります)

同様に、浮動小数点数に対して のように言う代わりに、 などif (a == b)のように言います。if (abs(a - b) < EPSILON)

この問題を軽減する (必ずしも排除するわけではありません) 別の方法は、アプリケーションに「フェイルファスト」ロジックを実装することです。たとえば、上記のコードでは、最後まで計算distanceして最後に 0 かどうかを確認する代わりにif (mx < EPSILON && my < EPSILON)、計算distanceと両方がゼロの場合は残りをスキップします (その場合、結果がゼロになることがわかっているため)。状況をすばやく把握すればするほど、エラーが蓄積する機会が少なくなります (また、必要のない場合に、よりコストのかかる計算を行うことを避けることもできます)。

于 2019-02-21T00:52:48.790 に答える