为什么 gcc 在条件乘法的 std::vector<float> 向量化方面比 clang 差得多?

Vla*_*gan 30 c++ gcc vectorization avx compiler-optimization

考虑使用以下 float 循环,使用 -O3 -mavx2 -mfma 编译

for (auto i = 0; i < a.size(); ++i) {
    a[i] = (b[i] > c[i]) ? (b[i] * c[i]) : 0;
}
Run Code Online (Sandbox Code Playgroud)

Clang 在矢量化方面做得非常出色。它使用 256 位 ymm 寄存器,并了解 vblendps/vandps 之间的差异,以获得尽可能最佳的性能。

.LBB0_7:
        vcmpltps        ymm2, ymm1, ymm0
        vmulps  ymm0, ymm0, ymm1
        vandps  ymm0, ymm2, ymm0
Run Code Online (Sandbox Code Playgroud)

然而,海湾合作委员会的情况要糟糕得多。由于某种原因,它并没有比 SSE 128 位向量更好(-mprefer-vector-width=256 不会改变任何东西)。

.L6:
        vcomiss xmm0, xmm1
        vmulss  xmm0, xmm0, xmm1
        vmovss  DWORD PTR [rcx+rax*4], xmm0
Run Code Online (Sandbox Code Playgroud)

如果将其替换为普通数组(如指南中所示),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;
  }
}
Run Code Online (Sandbox Code Playgroud)

但是我没有找到如何使用可变长度 std::vector 来做到这一点。gcc 需要什么样的提示才能将 std::vector 向量化为 AVX?

来自 Godbolt 的源码,使用 gcc 13.1 和 clang 14.0.0

Pet*_*des 35

这不是std::vector问题所在,float而是 GCC 通常糟糕的默认设置-ftrapping-math应该将 FP 异常视为可见的副作用,但并不总是正确地做到这一点,并且错过了一些安全的优化。

在这种情况下,源中存在条件 FP 乘法,因此严格的异常行为可以避免在比较错误的情况下可能引发上溢、下溢、不精确或其他异常。

在这种情况下,GCC 使用标量代码正确地执行了此操作...ss是单标量,使用 128 位 XMM 寄存器的底部元素,根本没有矢量化。您的 asm 不是 GCC 的实际输出:它使用 加载两个元素vmovss,然后在之前的vcomiss结果上分支,因此如果不是 true,则不会发生乘法。因此,与您的“GCC”asm 不同,我认为 GCC 的实际 asm 正确实现了。 vmulssb[i] > c[i]-ftrapping-math

请注意,自动矢量化的示例使用int *args,而不是float*. 如果您将其更改为float*并使用相同的编译器选项,它也不会自动矢量化,即使使用float *__restrict ahttps://godbolt.org/z/nPzsf377b)。

@273K 的答案表明AVX-512float即使使用 也可以自动矢量化-ftrapping-math,因为 AVX-512 屏蔽 ( ymm2{k1}{z}) 抑制屏蔽元素的 FP 异常,不会从 C++ 抽象机中不会发生的任何 FP 乘法引发 FP 异常。


gcc -O3 -mavx2 -mfma -fno-trapping-math自动矢量化所有 3 个函数 ( 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;
  }
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)

顺便说一句,我建议-march=x86-64-v3使用 AVX2+FMA 功能级别。这还包括 BMI1+BMI2 之类的东西。我认为它仍然只是使用-mtune=generic,但希望将来可以忽略仅对没有 AVX2+FMA+BMI2 的 CPU 重要的调整事情。

这些std::vector函数比较庞大,因为我们没有使用float *__restrict a = avec.data();或类似承诺控制块指向的数据不重叠std::vector(并且大小不知道是向量宽度的倍数),但非清理无重叠情况的循环使用相同的//vmulps进行矢量化。vcmpltpsvandps


也可以看看:


调整源以使乘法无条件?不

如果无论条件如何,C 源代码中的乘法都会发生,那么 GCC 将被允许以有效的方式对其进行矢量化,而无需 AVX-512 掩码。

// still scalar asm with GCC -ftrapping-math which is a bug
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;
  }
}
Run Code Online (Sandbox Code Playgroud)

但不幸的是,GCC -O3 -march=x86-64-v3(带默认值和不带默认值的Godbolt-ftrapping-math)仍然使标量汇编只能有条件地相乘!

这是 中的一个错误-ftrapping-math。它不仅过于保守,错过了自动矢量化的机会:它实际上是有缺陷的,对于抽象机(或调试版本)实际执行的某些乘法没有引发 FP 异常。像这样的垃圾行为是-ftrapping-math不可靠的,并且可能不应该默认启用。


@Ovinus Real 的回答指出,GCC仍然可以通过屏蔽两个输入而不是输出来 -ftrapping-math自动矢量化原始源。从不引发任何 FP 异常,因此它基本上是模拟 AVX-512 零掩码。0.0 * 0.0

这会更昂贵并且有更多的延迟来隐藏乱序执行,但仍然比标量好得多,特别是当 AVX1 可用时,特别是对于在某种级别的缓存中很热的中小型阵列。

(如果使用内在函数编写,只需将输出屏蔽为零,除非您实际上想在循环后检查 FP 环境中的异常标志。)

在标量源中执行此操作不会导致 GCC 生成这样的 asm:GCC 会将其编译为相同的分支标量 asm,除非您使用-fno-trapping-math. 至少这次不是错误,只是错过了优化:b[i]*c[i]当比较为假时,这不会起作用。

// doesn't help, still scalar asm with GCC -ftrapping-math
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;
  }
}
Run Code Online (Sandbox Code Playgroud)


S.M*_*.M. 20

GCC 默认针对较旧的 CPU 架构进行编译。

设置-march=native启用使用 256 位 ymm 寄存器。

.L7:
        vmovups ymm1, YMMWORD PTR [rsi+rax]
        vmovups ymm0, YMMWORD PTR [rdx+rax]
        vcmpps  k1, ymm1, ymm0, 14
        vmulps  ymm2{k1}{z}, ymm1, ymm0
        vmovups YMMWORD PTR [rcx+rax], ymm2
Run Code Online (Sandbox Code Playgroud)

设置-march=x86-64-v4启用使用 512 位 zmm 寄存器。

.L7:
        vmovups zmm2, ZMMWORD PTR [rsi+rax]
        vcmpps  k1, zmm2, ZMMWORD PTR [rdx+rax], 14
        vmulps  zmm0{k1}{z}, zmm2, ZMMWORD PTR [rdx+rax]
        vmovups ZMMWORD PTR [rcx+rax], zmm0
Run Code Online (Sandbox Code Playgroud)

  • @VladislavKogan:AVX-512 屏蔽会抑制屏蔽元素中的 FP 异常,让 GCC 制作遵循“-ftrapping-math”(默认情况下处于启用状态)的矢量化汇编。这就是为什么它可以使用 AVX-512 进行矢量化,但如果不关闭“-ftrapping-math”则不能使用早期扩展进行矢量化。顺便说一句,允许 256 位矢量化的“-march=native”仅适用于具有 AVX-512 的 CPU,例如 Ice Lake 和 Zen 4。(在大多数 CPU 上,默认值为“-mprefer-vector-width=256”,但显然“ -march=x86-64-v4` 更喜欢向量宽度=512。) (10认同)