c++ - 比较函数中的 FMA 指令破坏了严格的弱排序属性
问题描述
C++ 命名要求 Compare要求比较函数遵循严格的弱排序属性。这用于许多标准库函数,例如std::sort。
许多现代 CPU 支持Fused-Multiply-Add 运算,可以有效地计算a + b * c
并且不会对乘法结果进行四舍五入。这意味着在某些情况下,FMA 操作的结果不同于单独的乘法和加法操作。GCC 的 FMA 代码生成由-ffp-contract
标志控制,默认启用。
当编译器在比较函数中生成 FMA 指令时,可能会出现严格弱排序属性(静默)不再保持的情况。这可能导致std::sort
其他功能在运行时失败和崩溃。
考虑以下示例:
struct S {
double a;
double b;
};
bool compare(const volatile S& s1, const volatile S& s2) { // Volatile to prevent compile-time computations
return s1.a * s1.b - s2.a * s2.b < 0.0;
}
int main() {
const double a = 1 + 0x1.0p-52;
const double b = 1 - 0x1.0p-52;
volatile S s1{a, b}; // Volatile to prevent compile-time computations
volatile S s2{1.0, 1.0};
std::cout << compare(s1, s2) << '\n';
std::cout << compare(s2, s1) << '\n';
}
预期的输出将是
1
0
但是,在支持 FMA 的平台上(并且启用时,这也是 GCC 的默认设置),结果是
0
0
(GCC 11.2, -O3 -march=haswell
)
清楚地表明弱排序属性未强制执行不再成立。
为什么默认启用此具有潜在危险的功能?除了禁用 FMA 代码生成之外,还可以做些什么来避免这种情况?
解决方案
确保严格的弱顺序的责任在于您,而不是编译器。如果您只是true
忽略参数而返回,这显然不是严格的弱顺序。编译器无法“强制”弱排序。
您的代码当然没有那么糟糕。它只包含一个隐藏的假设,即s1.a * s1.b - s2.a * s2.b < 0.0;
当且仅当s1.a * s1.b < s2.a * s2.b;
。这适用于有理数,而不适用于双精度数。原因是operator-
四舍五入到一个double
值。只有你的版本有operator-
同样,当比较的实现错误时,编译器无法“强制”弱排序。
请注意,如果您传递 option -ffast-math
,则编译器可能会s1.a * s1.b - s2.a * s2.b < 0.0;
变成s1.a * s1.b < s2.a * s2.b;
或相反。这是一个不安全的优化,所以编译器在获得许可之前不会这样做。但如果没有-ffast-math
,编译器将完全使用您编写的内容,即使您应该使用其他形式。
推荐阅读
- php - 如何调用 .bat 文件而不是将整个路径写入 .exe?
- python-3.x - 是否有生成表格的python模块
- php - 使用 Codeigniter 3 创建会话
- node.js - TypeORM:具有重复性的一对一关系
- java - Jsp页面获取下载而不是在gradle中渲染
- python - Python数组转置
- android - Android - 重构网络请求
- reactjs - 约束子元素
- python - 我可以使用 xsdata python 库(或您知道的更好的选择)将 python 数据对象(使用数据类)序列化为使用 xsd 的 xml 输出吗?
- pytorch - pytorch 中的 mask_fill 在这里做什么