昨日、私は自分のプロジェクトでバグを追跡していました.数時間後、多かれ少なかれ次のようなことをしているコードに絞り込みました:
#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()) であるため、解決策ではなく「部分的な回避策」になります...「浮動小数点を使用しない」(;