-mavx/-mavx2/-mavx512f(以及暗示它们与相关调整设置的-march=选项)让GCC使用AVX/AVX2/AVX-512指令来编译你认为是个好主意的任何东西代码,包括但不限于循环的自动矢量化,如果您也启用了该功能。
SSE 指令的其他用例(如果您告诉它启用了 AVX,GCC 将使用 AVX 编码)包括复制和零初始化结构和数组,以及内联小常量大小 memset 和 @ 的其他情况987654335@。还有标量 FP 数学,即使在 -O0 的 64 位代码中,-mfpmath=sse 是默认值。
使用-mavx 构建的代码通常无法在没有 AVX 的 CPU 上运行,即使未启用自动矢量化并且您没有使用任何 AVX 内在函数;它使 GCC 对每个 SIMD 指令使用 VEX 编码而不是传统的 SSE。另一方面,AVX2 通常不会被使用,除非在实际自动矢量化循环时使用。它与仅复制数据或标量 FP 数学无关。不过,如果启用了-mfma,GCC 将使用标量 FMA 指令。
例子on Godbolt
void ext(void *);
void caller(void){
int arr[16] = {0};
ext(arr);
}
double fp(double a, double b){
return b-a;
}
使用带有gcc -O2 -fno-tree-vectorize -march=haswell 的 AVX 指令进行编译,因为当启用 AVX 时,GCC 完全避免在任何地方使用 legacy-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
-m 选项不启用自动矢量化; -ftree-vectorize 可以做到这一点。 它在 -O3 及更高版本上运行。 (或者 -O2 使用 GCC12 及更高版本,如使用 clang。)
如果您想要使用启用的扩展进行自动矢量化,请同时使用-O3,最好使用-march=native 或-march=znver2 或其他东西,而不仅仅是-mavx2。 -march 也设置了调整选项,并且将启用您可能忘记的其他 ISA 扩展,例如 -mfma 和 -mbmi2。
-march=haswell(或只是-mtune=haswell)暗示的调整选项在旧的 GCC 上特别有用,当tune=generic 更关心没有 AVX2 的旧 CPU 时,或者执行未对齐的 256 位加载时在某些情况下,两个独立的部分是一个胜利:Why doesn't gcc resolve _mm256_loadu_pd as single vmovupd?
不幸的是,没有像 -mtune=generic-avx2 或 -mtune=enabled-extension 这样的东西仍然关心 AMD 和 Intel CPU,但不关心那些对于您启用的所有扩展来说太旧的 CPU。
当手动使用内在函数进行矢量化时,您只能将内在函数用于已启用的指令集。 (或者默认开启的,比如 SSE2,它是 x86-64 的基线,即使在现代 GCC 配置中使用 -m32 也经常启用。)
例如如果您使用_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 术语中,“目标”是您正在为其编译的机器。即-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 的一部分启用,甚至在 GCC12 及更高版本中在-O2 启用)对于 GCC 进行自动矢量化是必需的。和/或-fopenmp,如果代码有一些#pragma omp simd。 (如果您关心性能,您肯定总是至少需要-O2 或-Os;-O3 应该最快,但可能并非总是如此。有时 GCC 会错过优化错误,其中 -O3让事情变得更糟,或者在大型程序中,更大的代码大小可能会导致更多的 I-cache 和 I-TLB 未命中。)
在一般情况下进行自动矢量化和优化时,GCC 将(可能)使用您告诉它可用的任何指令集(带有-m 选项)。例如,-O3 -march=haswell 将使用 AVX2 + FMA 自动矢量化。 -O3 没有 -m 选项只会使用 SSE2 自动矢量化。
例如比较 on Godbolt GCC -O3 -march=nehalem (SSE4.2) 与 -march=znver2 (AVX2) 对整数数组求和。 (编译时常量大小以保持 asm 简单)。
如果你使用-O3 -mgeneral-regs-only(后一个选项通常只在内核代码中使用),GCC 仍然会自动向量化,但只有在它认为使用SWAR 是有利可图的情况下(例如数组的异或很简单)使用 64 位整数 regs,甚至使用 SWAR 位黑客来阻止/纠正字节之间进位的字节总和)
例如gcc -O1 -mavx 仍然只使用标量代码。
通常,如果您想要完全优化而不是自动矢量化,您会使用 -O3 -march=znver1 -fno-tree-vectorize 之类的东西
其他编译器
以上所有内容都适用于 clang,除了它不理解 -mgeneral-regs-only。 (我认为您需要-mno-mmx -mno-sse 和其他选项。)
(The Effect of Architecture When Using SSE / AVX Intrinisics 重复了其中的一些信息)
对于 MSVC / ICC,您可以将内在函数用于 ISA 扩展,而您没有告诉编译器它可以单独使用。例如,没有-arch:AVX 的MSVC -O2 将让它使用SSE2 自动矢量化(因为这是x86-64 的基线),并使用movaps 复制大约16 字节的结构或其他任何东西。
但是使用 MSVC 的目标选项样式,您仍然可以使用 SSE4 内部函数,如 _mm_cvtepi8_epi32 (pmovsxwd),甚至 AVX 内部函数,而无需告诉编译器它允许自己使用这些指令。
Older MSVC used to make really bad asm 当您使用没有-arch:AVX 的 AVX / AVX2 内部函数时,例如导致在同一个函数中混合 VEX 和 legacy-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 版本。
我不知道英特尔的新 OneAPI 编译器 ICX。我认为它是基于 LLVM 的,所以它可能更像是 clang。