我了解到一些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指令或微操作?
编译器允许合并分隔的加法和乘法,尽管这会改变最终结果(通过使其更精确)。
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 intrinsic: (AVX2 - Intel Haswell)
-
_mm_fmadd_pd()
, _mm256_fmadd_pd()
-
_mm_fmadd_ps()
,_mm256_fmadd_ps()
- 和无数其他的变化…
FMA4 intrinsic: (XOP - AMD推土机)
-
_mm_macc_pd()
,_mm256_macc_pd()
-
_mm_macc_ps()
,_mm256_macc_ps()
- 和无数其他的变化…
我在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开始它会这样做。我不知道其他编译器什么时候开始这么做的