如何在SSE/AVX中使用融合乘法 - 加法(FMA)指令

40 c sse cpu-architecture avx fma

我已经了解到一些Intel/AMD CPU可以同时进行多次复用并添加SSE/AVX:
每个周期的FLOPS用于沙桥和haswell SSE2/AVX/AVX2.

我想知道如何在代码中做到最好,我也想知道它是如何在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));
...
Run Code Online (Sandbox Code Playgroud)

我的问题是如何将其转换为同时乘法并添加?数据可以依赖吗?我的意思是CPU可以_mm_add_ps(sum, _mm_mul_ps(a1, b1))同时执行还是在乘法中使用的寄存器和add必须是独立的?

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

Mys*_*ial 41

允许编译器融合分离的加法和乘法,即使这会改变最终结果(通过使其更准确).

FMA只有一个舍入(它有效地保持内部临时乘法结果的无限精度),而ADD + MUL有两个.

IEEE和C标准#pragma STDC FP_CONTRACT ON在生效时允许这样做,并且允许编译器ON默认使用它(但不是全部都可以).默认情况下-std=gnu*,Gcc收缩为FMA(默认情况下,但不是-std=c*,例如-std=c++14). 对于Clang来说,它只能启用-ffp-contract=fast.(仅使用#pragma启用,仅在单个表达式中a+b*c,而不是在单独的C++语句中.).

这与严格与宽松浮点(或gcc术语,-ffast-mathvs. -fno-fast-math)不同,这将允许其他类型的优化,这可能会根据输入值增加舍入误差.这是特殊的,因为FMA内部临时的无限精度; 如果在内部临时中存在任何四舍五入,则严格的FP中不允许这样做.

即使您启用了宽松的浮点,编译器仍可能选择不融合,因为如果您已经在使用内在函数,它可能会让您知道自己在做什么.


因此,确保实际获得所需FMA指令的最佳方法是实际使用提供的内在函数:

FMA3内在函数:(AVX2 - Intel Haswell)

  • _mm_fmadd_pd(),_mm256_fmadd_pd()
  • _mm_fmadd_ps(), _mm256_fmadd_ps()
  • 以及大量其他变化......

FMA4内在函数:(XOP - AMD推土机)

  • _mm_macc_pd(), _mm256_macc_pd()
  • _mm_macc_ps(), _mm256_macc_ps()
  • 以及大量其他变化......

  • 您只需将它们分开即可达到最大吞吐量。关键路径在于添加。`addps` 的延迟是 3 个周期。但吞吐量为 1。因此您至少需要 3 个独立的求和链才能充分利用它。您目前有 4 个,所以就足够了。 (2认同)

Z b*_*son 14

我在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);
}
Run Code Online (Sandbox Code Playgroud)

使用正确的编译器选项(见下文),每个编译器都将生成一条vfmadd指令(例如vfmadd213ss)mul_add.但是,只有MSVC无法收缩mul_addv到单个vfmadd指令(例如vfmadd213ps).

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

GCC:   -O2 -mavx2 -mfma
Clang: -O1 -mavx2 -mfma -ffp-contract=fast
ICC:   -O1 -march=core-avx2
MSVC:  /O1 /arch:AVX2 /fp:fast
Run Code Online (Sandbox Code Playgroud)

GCC 4.9不会收缩mul_addv到单个fma指令,但至少从GCC 5.1开始.我不知道其他编译器何时开始这样做.