【发布时间】:2017-06-27 04:48:54
【问题描述】:
昨天我正在跟踪我的项目中的一个错误,几个小时后,我已经缩小到一段代码,它或多或少做了这样的事情:
#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 次减法(我希望得到两个零),然后将它们平方(再次两个零)并将它们加在一起(零) .
事实证明,问题的根本原因是使用了 fused-multiply-add 操作,这使得结果不准确(从我的角度来看)。一般来说,我不反对这种优化,因为它承诺提供更准确的结果,但在这种情况下,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,因为 fused-multiply-add 无论如何都在编译器库内部使用(我看到在 sinf() 和 cosf()) 中有很多,所以这将是一个“部分解决方法”,而不是一个解决方案......我也想避免像“不要使用浮点”这样的解决方案(;
【问题讨论】:
-
浮点数/算术可能不精确。这是浮点运算的一个众所周知的“特性”。
-
@barny - 我知道,但是对于减去两个相同的数字或将任何东西乘以零浮点算术是非常准确的。 “是” - 因为使用 fused-multiply-add 不再是这种情况......而且我认为这里的错误规模很大。如果我得到像 1e-64 这样的东西,那我就不会问这个问题了……
-
GCC(我认为 ICC 也是)合同,但 Clang 默认情况下没有。我asked a question about this 因为我很惊讶 GCC 会这样做。显然,很多人也没有想到这一点。事实证明它没有违反 IEEE,所以 GCC 仍然符合这样做。
-
我认为有两种可能的解决方案需要考虑。 1.) 仅显式使用 FMA。这意味着您使用
-ffp-contract=off -mfma进行编译,然后仅在需要时使用fma函数或内在函数来获取FMA。 2.) 设计您的代码,使其处理带有和不带有 FMA 操作的浮点错误,使其对 FMA 操作不敏感。 -
您可以在问题中的测试中添加
float mx_fma = fmaf(r, cosf(alpha), -r*cosf(alpha))。它应该产生相同的结果。然后你可以用-ffp-contract=off编译,看看你得到了什么。您可能不会从中学到任何您不期望的东西,但我认为尝试一下很有趣。
标签: c++ floating-point precision floating-accuracy fma