考虑以下浮点循环,使用-O3 -mavx 2-mfma编译
for (auto i = 0; i < a.size(); ++i) {
a[i] = (b[i] > c[i]) ? (b[i] * c[i]) : 0;
}
字符串
Clang把它矢量化得很完美它使用256位ymm寄存器,并了解vblendps/vandps之间的差异,以实现最佳性能。
.LBB0_7:
vcmpltps ymm2, ymm1, ymm0
vmulps ymm0, ymm0, ymm1
vandps ymm0, ymm2, ymm0
型
GCC,但是,要糟糕得多。出于某种原因,它并不比SSE 128位向量更好(-mprefer-vector-width=256不会改变任何内容)。
.L6:
vcomiss xmm0, xmm1
vmulss xmm0, xmm0, xmm1
vmovss DWORD PTR [rcx+rax*4], xmm0
型
如果将其替换为普通数组(as in guideline),则gcc会将其矢量化为AVX ymm。
int a[256], b[256], c[256];
auto foo (int *a, int *b, int *c) {
int i;
for (i=0; i<256; i++){
a[i] = (b[i] > c[i]) ? (b[i] * c[i]) : 0;
}
}
型
然而,我没有找到如何用可变长度的std::vector来实现它。gcc需要什么样的提示才能将std::vector矢量化到AVX?
Source on Godbolt with gcc 13.1 and clang 14.0.0的
3条答案
按热度按时间u2nhd7ah1#
这不是
std::vector
的问题,这是float
和GCC的-ftrapping-math
的通常不好的默认值,应该将FP异常视为可见的副作用,但并不总是正确地这样做,并错过了一些安全的优化。在这种情况下,在源代码中有一个条件FP乘法,因此严格的异常行为避免了在比较为假的情况下可能引发溢出,下溢,不精确或其他异常。
GCC在本例中使用标量代码正确地完成了这一操作:
...ss
是Scalar Single,使用128位XMM寄存器的底部元素,根本没有矢量化。你的asm不是GCC的实际输出:它用vmovss
加载两个元素,然后在vmulss
之前的vcomiss
结果上分支,所以如果b[i] > c[i]
不为真,乘法就不会发生。因此,与您的“GCC”asm不同,我认为GCC的实际asm正确地实现了-ftrapping-math
。请注意,您的自动矢量化示例使用的是
int *
args,而不是float*
。如果将其更改为float*
并使用相同的编译器选项,它也不会自动向量化,即使使用float *__restrict a
(https://godbolt.org/z/nPzsf377b)也是如此。@273K的答案表明AVX-512允许
float
自动向量化,即使使用-ftrapping-math
,因为AVX-512掩码(ymm2{k1}{z}
)抑制了掩码元素的FP异常,不会从任何FP乘法中引发FP异常,而这些FP乘法在C++抽象机中不会发生。gcc -O3 -mavx2 -mfma -fno-trapping-math
自动矢量化所有3个函数(Godbolt)个字符
顺便说一句,**我推荐
-march=x86-64-v3
**用于AVX 2 +FMA功能级别。这也包括BMI 1 + BMI 2和东西。我认为它仍然只是使用-mtune=generic
,但希望将来可以忽略那些只对没有AVX 2 +FMA+ BMI 2的CPU重要的调整。std::vector
函数更庞大,因为我们没有使用float *__restrict a = avec.data();
或类似的东西来保证std::vector
控制块指向的数据不重叠(并且大小不知道是向量宽度的倍数),但是对于不重叠的情况,非清理循环使用相同的vmulps
/vcmpltps
/vandps
进行向量化。参见:
-ftrapping-math
坏了,根据GCC开发人员Marc Glisse的说法,“从未工作过”。但是从2012年开始,https://gcc.gnu.org/bugzilla/show_bug.cgi?id=54192提议将其设为非默认值仍然是开放的。-ffast-math
之外的各种FP选项,例如-fno-math-errno
,它允许许多函数内联,并且对于在调用sqrt
或其他东西之后不检查errno
的正常代码来说不是问题!)-ffast-math
或#pragma omp simd reduction (+:my_sum_var)
进行向量化,但@phuclv的答案有一些很好的链接)修改源码,让乘法变成无条件?不知道
如果C源代码中的乘法无论条件如何都会发生,那么GCC将 * 允许 * 在没有AVX-512掩码的情况下以有效的方式对其进行向量化。
型
但不幸的是,GCC
-O3 -march=x86-64-v3
(Godbolt有和没有默认的-ftrapping-math
)仍然使标量asm只能有条件地相乘!这是
-ftrapping-math
中的bug。它不仅过于保守,错过了自动矢量化的机会:它实际上是错误的,* 不是 * 为抽象机器(或调试构建)实际执行的某些乘法引发FP异常。像这样的垃圾行为就是为什么-ftrapping-math
不可靠,并且可能不应该在默认情况下打开。@Ovinus真实的的回答指出,GCC
-ftrapping-math
仍然可以通过屏蔽 * 两个输入 * 而不是输出来自动矢量化原始源代码。0.0 * 0.0
从不引发任何FP异常,因此它基本上是在模拟AVX-512零掩码。这将是更昂贵的,并有更多的延迟,无序执行隐藏,但仍然比标量好得多,特别是当AVX 1可用时,特别是对于中小型阵列,在某些级别的高速缓存中是热的。
(If使用intrinsic编写,只需将输出屏蔽为零,除非您确实希望在循环后检查FP环境中的异常标志。)
在标量源代码中这样做不会导致GCC像那样制作asm:GCC将其编译为相同的分支标量asm,除非您使用
-fno-trapping-math
。至少这不是一个bug,只是一个错过的优化:当比较结果为false时,不会执行b[i]*c[i]
。型
628mspwn2#
GCC默认为较旧的CPU架构编译。
设置
-march=native
可启用256位ymm寄存器。字符串
设置
-march=x86-64-v4
可启用512位zmm寄存器。型
093gszye3#
假设-ftrapping-math,另一个选项是在将它们相乘之前将忽略的输入置零(未测试):
字符串
这当然转化为更大的宽度。
两个输入都必须归零,因为如果x < 0,则+0.0 * x为-0.0。在某些处理器上,这可能与相同向量宽度的其他解决方案具有相同的吞吐量。同样的方法也适用于加法、减法和平方根。除法需要除零以外的除数。
即使在fno-trapping-math下,该解决方案也可能略上级乘法之后的一个掩蔽,因为它避免了与需要微编码乘法的被忽略的输入相关联的惩罚。但我不确定吞吐量是否可以与乘法后为零的版本相同。