将融合乘法累加用于双精度有多有利?

How advantageous is using fused multiply-accumulate for double-precision?

我试图通过查看生成的汇编代码来了解使用带有双参数的 std::fma 是否有利,我正在使用标志“-O3 ",我正在比较这两个例程的程序集:

#include <cmath>
#define FP_FAST_FMAF

float test_1(const double &a, const double &b, const double &c ){
    return a*b + c;
}
float test_2(const double &a, const double &b, const double &c ){
    return std::fma(a,b,c);
}

使用 Compiler Explorer 工具,这是为两个例程生成的程序集:

test_1(double const&, double const&, double const&):
        movsd     xmm0, QWORD PTR [rdi]                         #5.12
        mulsd     xmm0, QWORD PTR [rsi]                         #5.14
        addsd     xmm0, QWORD PTR [rdx]                         #5.18
        cvtsd2ss  xmm0, xmm0                                    #5.18
        ret                                                     #5.18
test_2(double const&, double const&, double const&):
        push      rsi                                           #7.65
        movsd     xmm0, QWORD PTR [rdi]                         #8.12
        movsd     xmm1, QWORD PTR [rsi]                         #8.12
        movsd     xmm2, QWORD PTR [rdx]                         #8.12
        call      fma                                           #8.12
        cvtsd2ss  xmm0, xmm0                                    #8.12
        pop       rcx                                           #8.12
        ret      

并且程序集不会因使用可用于 icc 或 gcc 的最新版本而改变。关于这两个例程的性能,令我困惑的是,test_1 只有一个内存操作(movsd),[=25= 有三个],考虑到内存操作的延迟比浮点操作的延迟大一到两个数量级,test_1 应该更高效。因此,在什么情况下建议使用 std::fma?我的假设有什么错误?

如果您的问题仅与内存操作数有关,请务必注意 mulsdaddsd 在您的示例中也是内存操作。内存操作由寄存器名称周围的方括号指示,而不是汇编助记符本身。

如果您仍然好奇使用 std::fma 是否有利,答案可能是 "it depends."

当您通过查看汇编来分析性能时,向编译器提供至少一些关于您的目标体系结构的信息几乎是必不可少的。 std::fma 使用硬件 FMA 指令(如果它们在目标体系结构上可用),因此 std::fma 是否总体上提高了性能并不是一个真正可以回答的问题。

如果你specify -mfma in Compiler Explorer,编译器有一些信息可以用来生成更高效的代码。您还可以指定 -march=[your architecture] 如果支持,它将自动为您设置 -mfma


此外,由于使用浮点数处理舍入的方式,std::fma(a*b)+c 的结果存在细微差异,还有一大堆问题。 std::fma两次浮点运算只取整一次,而(a*b)+c可能[1]a*b,将结果存入64位,加c 到这个值,然后将结果存储在 64 位中。

如果您想在计算中最大限度地减少浮点运算错误,std::fma 可能是更好的选择,因为它保证您只会将宝贵的位从您宝贵的浮点数中剥离一次。


[1] 是否进行这种额外舍入取决于您的编译器、优化设置和体系结构设置: Compiler Explorer msvc、gcc、icc、clang 示例