gcc 编译器开关 (-mavx -mavx2 -mavx512f) 到底有什么作用?

JB_*_*ser 5 gcc simd instruction-set avx avx512

我在 C/C++ 代码中明确使用了英特尔 SIMD 内在扩展。为了编译代码,我需要在命令行上指定 -mavx、-mavx512 或类似的内容。我对这一切都很满意。

然而,从阅读 gcc 手册页来看,并不清楚这些命令行标志是否也告诉 gcc 编译器尝试使用英特尔 SIMD 指令自动矢量化 C/C++ 代码。有人知道情况是否如此吗?-mavx 标志只是允许您手动将 SIMD 内在函数插入代码中,还是还告诉编译器在编译 C/C++ 代码时使用 SIMD 指令?

Pet*_*des 15

-mavx// -mavx2-mavx512f以及-march=暗示它们具有相关调整设置的选项)让 GCC 在编译代码时使用 AVX / AVX2 / AVX-512 指令来执行它认为是个好主意的任何内容,包括但不限于循环的自动矢量化,如果您也启用该功能。

SSE 指令的其他用例(如果您告诉 GCC 启用了 AVX,GCC 将使用 AVX 编码)包括复制和零初始化结构体和数组,以及内联小型常量大小memsetmemcpy. 还有标量 FP 数学,即使在-O064 位代码中-mfpmath=sse也是默认值。

使用 AVX 构建的代码-mavx通常无法在没有 AVX 的 CPU 上运行,即使未启用自动矢量化并且您没有使用任何 AVX 内在函数;它使 GCC 对每个 SIMD 指令使用 VEX 编码而不是传统的 SSE。另一方面,除非实际自动矢量化循环,否则通常不会使用 AVX2。它与复制数据或标量 FP 数学无关。-mfma不过,如果启用,GCC 将使用标量 FMA 指令。

Godbolt 的示例

void ext(void *);
void caller(void){
    int arr[16] = {0};
    ext(arr);
}

double fp(double a, double b){
    return b-a;
}
Run Code Online (Sandbox Code Playgroud)

使用 AVX 指令进行编译gcc -O2 -fno-tree-vectorize -march=haswell,因为当启用 AVX 时,GCC 完全避免到处使用传统的 SSE 编码。

caller:
        sub     rsp, 72
        vpxor   xmm0, xmm0, xmm0
        mov     rdi, rsp
        vmovdqa XMMWORD PTR [rsp], xmm0         # only 16-byte vectors, not using YMM + vzeroupper
        vmovdqa XMMWORD PTR [rsp+16], xmm0
        vmovdqa XMMWORD PTR [rsp+32], xmm0
        vmovdqa XMMWORD PTR [rsp+48], xmm0
        call    ext
        add     rsp, 72
        ret

fp:
        vsubsd  xmm0, xmm1, xmm0
        ret
Run Code Online (Sandbox Code Playgroud)

-m选项不启用自动矢量化;-ftree-vectorize这样做。 它处于-O3甚至更高的位置。(或者在 GCC12 及更高版本的有限形式中-O2,仅在“非常便宜”时进行矢量化,例如当它知道迭代计数是 4 的倍数或其他什么时,这样它就可以在没有清理循环的情况下进行矢量化。clang 完全启用自动矢量化-O2。 )

如果您确实想要启用扩展的自动矢量化,-O3也可以使用,最好是-march=nativeor-march=znver2或其他东西,而不仅仅是-mavx2. -march还设置调整选项,并将启用您可能忘记的其他 ISA 扩展,例如-mfma-mbmi2

当更多地关心没有 AVX2 的旧 CPU 时,或者在某些情况下将未对齐的 256 位加载作为两个单独的部分是一种胜利时,-march=haswell(或只是)暗示的调整选项-mtune=haswell在旧版 GCC 上特别有用:为什么不gcc 不会将 _mm256_loadu_pd 解析为单个 vmovupd 吗?tune=generic

不幸的是,没有任何东西像-mtune=generic-avx2-mtune=enabled-extension仍然关心AMD和Intel CPU,但不关心那些对于您启用的所有扩展来说太旧的CPU。


使用内部函数手动矢量化时,您只能将内部函数用于已启用的指令集。(或者是默认打开的,例如 SSE2,它是 x86-64 的基线,甚至-m32在现代 GCC 配置中也经常启用。)

例如,如果您使用_mm256_add_epi32,则除非您使用 ,否则您的代码将无法编译-mavx2。(或者更好的是,类似-march=haswell或 的东西-march=native可以启用 AVX2、FMA、BMI2 和现代 x86 拥有的其他功能,设置适当的调整选项。)

在这种情况下,GCC 错误消息是error: inlining failed in call to 'always_inline' '_mm256_loadu_si256': target specific option mismatch

在 GCC 术语中,“目标”是您正在编译的机器。ie-mavx2告诉 GCC 目标支持 AVX2。 因此,GCC 将制作一个可以在任何地方使用 AVX2 指令的可执行文件,例如用于复制结构或对本地数组进行零初始化,或者以其他方式扩展小型常量大小的 memcpy 或 memset。

它还会定义CPP宏__AVX2__,因此#ifdef __AVX2__可以测试编译时是否可以假定AVX2。

如果这不是您想要的整个程序,您需要确保不要使用-mavx2编译任何在没有运行时检查 CPU 功能的情况下调用的代码。例如,将 AVX2 版本的函数放在单独的文件中进行编译-mavx2或使用__attribute__((target("avx2")))。让你的程序在检查后设置函数指针__builtin_cpu_supports("avx2"),或者使用GCC的ifunc调度机制来进行多版本控制。


-m选项本身并不启用自动矢量

(自动矢量化并不是 GCC 使用 SIMD 指令集的唯一方式。)

-ftree-vectorize(作为 的一部分启用-O3,甚至在-O2GCC12 及更高版本中启用)对于 GCC 自动矢量化是必要的。和/或-fopenmp如果代码有一些#pragma omp simd. (你肯定总是想要至少-O2或者-Os如果你关心性能;-O3 应该是最快的,但可能并不总是如此。有时,GCC 会出现错过优化的错误,其中 -O3 会使事情变得更糟,或者在大型程序中,可能会发生较大的代码大小会导致更多 I-cache 和 I-TLB 未命中。)

一般来说,当自动矢量化和优化时,GCC(可能)会使用您告诉它可用的任何指令集(带有-m选项)。例如,-O3 -march=haswell将使用 AVX2 + FMA 自动矢量化。 -O3没有-m选项将仅使用 SSE2 自动矢量化。

例如,比较Godbolt GCC -O3 -march=nehalem(SSE4.2) 与-march=znver2(AVX2) 对整数数组求和。(编译时常量大小以保持汇编简单)。

如果您使用-O3 -mgeneral-regs-only(后一个选项通常仅在内核代码中使用),GCC 仍然会自动矢量化,但仅在它认为执行SWAR有利可图的情况下(例如,使用 64 位整数寄存器对数组进行异或很简单,或者使用 SWAR 位黑客来阻止/纠正字节之间的进位的偶数字节总和)

例如gcc -O1 -mavx仍然只使用标量代码。

通常,如果您想要完全优化而不是自动矢量化,您可以使用类似的东西-O3 -march=znver1 -fno-tree-vectorize


其他编译器

上述所有内容对于 clang 来说也都是正确的,只是它不理解-mgeneral-regs-only。(我认为您-mno-mmx -mno-sse可能需要其他选择。)

使用 SSE / AVX Intrinisics 时的架构效果重复了其中的一些信息)

对于 MSVC / ICC,您可以使用 ISA 扩展的内在函数,但您没有告诉编译器它可以单独使用。例如,如果-O2没有MSVC -arch:AVX,它会使用 SSE2 自动矢量化(因为这是 x86-64 的基线),并用于movaps复制 16 字节结构或其他内容。

但是,使用 MSVC 的目标选项样式,您仍然可以使用 SSE4 内在函数(如_mm_cvtepi8_epi32( pmovsxwd)),甚至 AVX 内在函数,而无需告诉编译器允许它本身使用这些指令。

当您使用不带 AVX / AVX2 内在函数时,较旧的 MSVC 曾经会做出非常糟糕的汇编-arch:AVX,例如导致在同一函数中混合 VEX 和旧版 SSE 编码(例如,对 128 位内在函数使用非 VEX 编码,如_mm_add_ps),并且无法在 256 位向量之后使用 vzeroupper,这两者都会对性能造成灾难性的影响。

但我认为现代 MSVC 已经基本解决了这个问题。尽管它仍然没有对内在函数进行太多优化,甚至没有通过它们进行恒定传播。

不优化内在函数可能与 MSVC 让您编写类似代码的能力有关if(avx_supported) { __m256 v = _mm256_load_ps(p); ... 。如果它试图优化,它必须跟踪沿着执行路径已经看到的可以到达任何给定内在的最小扩展级别,这样它就知道哪些替代方案是有效的。ICC也是这样。

出于同样的原因,GCC 无法将具有不同目标选项的函数相互内联。所以你不能用它__attribute__((target("")))来避免运行时调度的成本;您仍然希望避免循环内的函数调用开销,即确保 AVX2 函数内有循环,否则可能不值得拥有 AVX2 版本,只需使用 SSE2 版本。

我不知道Intel新的OneAPI编译器ICX。我认为它是基于 LLVM 的,所以它可能更像 clang。