如何在SSE/AVX中使用Fused Multiply-Add(FMA)指令

48

我了解到一些Intel/AMD CPU可以使用SSE/AVX进行并行乘加操作:
Sandy Bridge和Haswell SSE2/AVX/AVX2每个周期的FLOPS

我想知道如何在代码中最好地实现这个操作,并想了解CPU内部是如何实现的,也就是使用超标量架构。比如说我想在SSE中执行以下长加法:

//sum = a1*b1 + a2*b2 + a3*b3 +... where a is a scalar and b is a SIMD vector (e.g. from matrix multiplication)
sum = _mm_set1_ps(0.0f);
a1  = _mm_set1_ps(a[0]); 
b1  = _mm_load_ps(&b[0]);
sum = _mm_add_ps(sum, _mm_mul_ps(a1, b1));

a2  = _mm_set1_ps(a[1]); 
b2  = _mm_load_ps(&b[4]);
sum = _mm_add_ps(sum, _mm_mul_ps(a2, b2));

a3  = _mm_set1_ps(a[2]); 
b3  = _mm_load_ps(&b[8]);
sum = _mm_add_ps(sum, _mm_mul_ps(a3, b3));
...

我的问题是如何将其转换为同时乘法和加法?数据是否可以相互依赖?也就是说,CPU是否可以同时执行 _mm_add_ps(sum, _mm_mul_ps(a1, b1)) ,还是乘法和加法所使用的寄存器必须是独立的?

最后,这如何应用于FMA(Haswell)?_mm_add_ps(sum, _mm_mul_ps(a1, b1)) 是否会自动转换为单个FMA指令或微操作?

2个回答

50
编译器可以合并分离的加法和乘法,即使这会改变最终结果(使其更准确)。
FMA只有一个舍入(它有效地保持了内部临时乘积结果的无限精度),而ADD + MUL有两个。
#pragma STDC FP_CONTRACT ON生效时,IEEE和C标准允许这样做,编译器允许默认启用ON(但并非所有编译器都是如此)。 Gcc默认情况下使用FMA进行合同(使用默认的-std=gnu*,但不使用-std=c*,例如-std=c++14)。 对于Clang, 只有使用-ffp-contract = fast才会启用。 (仅在单个表达式中启用#pragma,例如a+b*c,而不跨越单独的C ++语句。)
这与严格和松散的浮点数不同(或在gcc术语中,-ffast-math-fno-fast-math),后者允许其他种类的优化可能会增加舍入误差,具体取决于输入值。这个特殊之处在于FMA内部临时变量的无限精度;如果在内部临时变量中有任何舍入,这将不被允许在严格FP中使用。
即使启用了松散的浮点数,编译器也可能选择不进行融合,因为它可能认为如果您已经使用了内置函数,则应该知道自己在做什么。

因此,确保您获取所需的FMA指令的最佳方法是实际使用为其提供的内部函数:

FMA3内部函数:(AVX2-英特尔Haswell)

  • _mm_fmadd_pd(),_mm256_fmadd_pd()
  • _mm_fmadd_ps()_mm256_fmadd_ps()
  • 以及其他无数变体...

FMA4内部函数:(XOP-AMD Bulldozer)

  • _mm_macc_pd()_mm256_macc_pd()
  • _mm_macc_ps()_mm256_macc_ps()
  • 还有无数其他变体...

1
谢谢,这正是我想的。现在我只需要想办法组织我的代码,使得像我上面定义的总和可以同时进行独立的加法和乘法(以避免延迟)。 - user2088790
2
你只需要将它们分开,以达到最大吞吐量。关键路径在加法上。addps 的延迟为 3 个时钟周期。但吞吐量为 1。因此,您需要至少 3 条独立的求和链来充分利用它。您目前有 4 条,所以这是足够的。 - Mysticial
gcc在实践中确实会在-O3下融合mul/add内置函数,即使在FLT_EVAL_METHOD=0而不是2的情况下也是如此。这得到了一些文档的支持(保持临时无限精度始终是合法的,以允许折叠表达式)。关于这个问题有一个新问题 - Peter Cordes
你确定“编译器会通过融合来违反严格的IEEE浮点行为”吗?我不太确定了。我认为可能IEEE支持多种“模式”,其中一种允许单舍入缩减。GCC默认情况下在32位代码中使用x87,我不认为这违反了严格的IEEE行为。GCC默认情况下在32位代码中使用x87,在64位代码中使用SSE,在FMA硬件中使用FMA。我不认为这些情况中的任何一种都必然违反IEEE。另一方面,Clang在所有线程情况下默认使用SSE,因此更加一致。 - Z boson
1
我认为你的答案是误导性的,因为编译器可以默认使用FMA而不违反IEEE规则。 - Z boson
显示剩余17条评论

21

我在 GCC 5.3、Clang 3.7、ICC 13.0.1 和 MSVC 2015(编译器版本 19.00)中测试了以下代码。

float mul_add(float a, float b, float c) {
    return a*b + c;
}

__m256 mul_addv(__m256 a, __m256 b, __m256 c) {
    return _mm256_add_ps(_mm256_mul_ps(a, b), c);
}

通过正确的编译器选项(请参见下文),每个编译器都会从mul_add生成一个vfmadd指令(例如,vfmadd213ss)。但是,只有MSVC无法将mul_addv缩减为单个vfmadd指令(例如,vfmadd213ps)。

以下编译器选项足以生成vfmadd指令(除了使用MSVC的mul_addv)。

GCC:   -O2 -mavx2 -mfma
Clang: -O1 -mavx2 -mfma -ffp-contract=fast
ICC:   -O1 -march=core-avx2
MSVC:  /O1 /arch:AVX2 /fp:fast

自GCC 4.9以来,它不会将mul_addv简化为单个fma指令,但自至少GCC 5.1以来,它这样做了。我不知道其他编译器何时开始这样做。


另请参阅 #pragma STDC FP_CONTRACT ON。Stephen Canon 指出它只允许在单个语句内进行缩减,而不能跨越多个语句。(http://lists.llvm.org/pipermail/cfe-dev/2015-September/045110.html)。还要注意,gcc 仅在 -std=gnu* 下启用缩减,而不是 -std=c11 或其他选项。(然后它会在 IEEE + ISO C 严格规定之外跨越多个语句进行缩减)。尝试使用单独变量的另一个测试函数可能值得一试。 - Peter Cordes
@PeterCordes,请查看此链接 https://dev59.com/WlsW5IYBdhLWcg3w8q1S 和 Stephen Canon 的回答。根据Stephen的回答,我认为GCC所做的是可以的(假设GCC没有忽略STDC FP_CONTRACT,但很遗憾上次我检查时它确实忽略了)。 - Z boson
你的问题只涉及到 return a*b + c;,而没有涉及 float mul = a*b; return mul + c;。仔细阅读Stephen在邮件列表中的帖子:他提到clang的STDC FP_CONTRACT ON只在表达式内启用合并,而不像clang的-ffp-contract=fast会在此注释中启用。这就是为什么clang为命令行选项有单独的onfast设置。请参阅我最近对此问题Mysticial答案的编辑。这比我一开始想象的要麻烦些:( - Peter Cordes
@PeterCordes,我的一个观点是GCC忽略了#pragma STDC FP_CONTRACT。至少上次我检查时是这样的。我应该再次检查(例如gnuc99和c99或其他)。 - Z boson
我认为这仍然是正确的。它的实际行为超出了#pragma STDC FP_CONTRACT ON所允许的范围,因此它不完全像将其默认设置为ON并未提供关闭它的方法。从我所读到的内容来看,IEEE + C并没有指定#pragma STDC FP_CONTRACT FAST,尽管这是一个有用的设置。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接