首页 > 解决方案 > 比较函数中的 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 代码生成之外,还可以做些什么来避免这种情况?

标签: c++gcc

解决方案


确保严格的弱顺序的责任在于您,而不是编译器。如果您只是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,编译器将完全使用您编写的内容,即使您应该使用其他形式。


推荐阅读