这个问题不在于`std::vector`,而是在于`float`和GCC通常糟糕的默认设置`-ftrapping-math`,它应该将浮点异常视为可见的副作用,但并不总是正确地做到这一点,并且会错过一些安全的优化。
在这种情况下,源代码中存在一个条件性的浮点乘法,因此严格的异常行为可以避免在比较结果为假时可能引发溢出、下溢、不精确或其他异常。
在这种情况下,GCC使用标量代码来正确处理:`...ss`是标量单精度浮点数,使用128位XMM寄存器的底部元素,根本没有向量化。你的汇编代码不是GCC的实际输出:它使用`vmovss`将两个元素都加载进来,然后在`vcomiss`结果之前进行分支判断,所以如果`b[i] > c[i]`不成立,乘法就不会发生。因此,与你的“GCC”汇编代码不同,我认为GCC的实际汇编代码正确地实现了`-ftrapping-math`。
请注意,您的示例中使用了
int *
参数进行自动向量化,而不是
float*
。如果您将其更改为
float*
并使用相同的编译器选项,即使使用
float *__restrict a
(
https://godbolt.org/z/nPzsf377b),它也不会自动向量化。
@273K的答案表明,AVX-512即使使用
-ftrapping-math
,也可以让
float
进行自动向量化,因为AVX-512掩码(
ymm2{k1}{z}
)抑制了掩码元素的FP异常,不会引发任何在C++抽象机器中未发生的FP乘法的FP异常。
gcc -O3 -mavx2 -mfma -fno-trapping-math
自动向量化了所有三个函数(Godbolt)
void foo (float *__restrict a, float *__restrict b, float *__restrict c) {
for (int i=0; i<256; i++){
a[i] = (b[i] > c[i]) ? (b[i] * c[i]) : 0;
}
}
foo(float*, float*, float*):
xor eax, eax
.L143:
vmovups ymm2, YMMWORD PTR [rsi+rax]
vmovups ymm3, YMMWORD PTR [rdx+rax]
vmulps ymm1, ymm2, YMMWORD PTR [rdx+rax]
vcmpltps ymm0, ymm3, ymm2
vandps ymm0, ymm0, ymm1
vmovups YMMWORD PTR [rdi+rax], ymm0
add rax, 32
cmp rax, 1024
jne .L143
vzeroupper
ret
顺便说一下,我建议使用-march=x86-64-v3
来实现AVX2+FMA特性级别。这还包括了BMI1+BMI2等功能。我认为它仍然只使用-mtune=generic
,但希望将来可以忽略那些仅对没有AVX2+FMA+BMI2的CPU重要的调整。
std::vector
函数变得更庞大,因为我们没有使用float *__restrict a = avec.data();
或类似的方式来保证指向std::vector
控制块的数据不重叠(并且大小未知是否是矢量宽度的倍数),但是对于无重叠情况的非清除循环仍然使用相同的vmulps
/ vcmpltps
/ vandps
进行矢量化。
另请参阅:
-ftrapping-math
是有问题的,根据GCC开发者Marc Glisse的说法,“从来没有起作用”。但是2012年提出的建议将其设置为非默认选项仍然未解决。
- 如何强制GCC假设浮点表达式为非负数?(除了完整的
-ffast-math
之外,还有其他各种浮点选项,比如-fno-math-errno
,它允许许多函数进行内联,并且对于在调用sqrt
或其他函数后不检查errno
的正常代码来说并不是问题!)
- GCC中浮点数运算的语义
- 双精度和
-ffast-math
上的自动向量化(当然,只有使用-ffast-math
或#pragma omp simd reduction (+:my_sum_var)
时才会对归约进行向量化,但@phuclv的答案中有一些很好的链接)
调整源代码使乘法无条件执行?不行。
如果C源代码中的乘法无论条件如何都会执行,那么GCC将被允许以高效的方式进行矢量化,而无需AVX-512掩码。
void foo (float *__restrict a, float *__restrict b, float *__restrict c) {
for (int i=0; i<256; i++){
float prod = b[i] * c[i];
a[i] = (b[i] > c[i]) ? prod : 0;
}
}
但不幸的是,GCC
-O3 -march=x86-64-v3
(
Godbolt 有和没有默认的
-ftrapping-math
)仍然生成只有条件乘法的标量汇编代码!
这是一个在`-ftrapping-math`中的错误。它不仅过于保守,错失了自动向量化的机会:实际上,它有缺陷,对于一些抽象机器(或调试版本)实际执行的乘法操作没有引发浮点异常。像这样的糟糕行为是为什么`-ftrapping-math`不可靠,可能不应该默认开启的原因。
@Ovinus Real's answer 指出GCC的-ftrapping-math
仍然可以通过屏蔽两个输入而不是输出来自动向量化原始源代码。 0.0 * 0.0
从不引发任何FP异常,因此基本上是模拟AVX-512零屏蔽。
这样做会更昂贵,并且对于乱序执行来说具有更多的延迟,但与标量相比仍然要好得多,尤其是当可用AVX1时,特别适用于在某个级别的缓存中热门的小到中等大小的数组。
(如果使用内部函数编写,请将输出屏蔽为零,除非您实际上想在循环后检查FP环境的异常标志。)
在标量源代码中执行此操作不会导致GCC生成类似的汇编代码:除非使用-fno-trapping-math
,否则GCC将将其编译为相同的分支标量汇编代码。至少这次不是一个错误,只是一个被忽视的优化:当比较为false时,它不执行b[i]*c[i]
。
void bar (float *__restrict a, float *__restrict b, float *__restrict c) {
for (int i=0; i<256; i++){
float bi = b[i];
float ci = c[i];
if (! (bi > ci)) {
bi = ci = 0;
}
a[i] = bi * ci;
}
}
ss
后缀表示“标量,单精度”)。如果实际上是向量化的SSE,后缀应该是ps
。 - haroldvcomiss
标量比较,而不是传统的SSEcomiss
),通过启用AVX来使用128位的XMM寄存器。 - Peter Cordesvcomiss
标量比较,而不是传统的SSEcomiss
),并且使用了128位的XMM寄存器,因为你启用了AVX。 - Peter Cordes