【问题标题】:What exactly do the gcc compiler switches (-mavx -mavx2 -mavx512f) do?gcc 编译器开关 (-mavx -mavx2 -mavx512f) 到底有什么作用?
【发布时间】:2022-07-31 17:51:12
【问题描述】:

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

但是,通过阅读 gcc 手册页,尚不清楚这些命令行标志是否还告诉 gcc 编译器尝试使用英特尔 SIMD 指令自动矢量化 C/C++ 代码。有人知道是不是这样? -mavx 标志是否只是允许您手动将 SIMD 内在函数插入代码中,还是它还告诉编译器在编译 C/C++ 代码时使用 SIMD 指令?

【问题讨论】:

  • 已经阅读了。这基本上是手册页。目前尚不清楚 gcc 是否尝试自动矢量化代码。至少对我来说不是。
  • 尝试与否无关。 These switches enable the use of instructions 结束。仅此而已。

标签: gcc simd avx instruction-set avx512


【解决方案1】:

-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。

【讨论】:

  • (这个答案的部分内容是多余的;它已经很长了,所以我从顶部重新开始,但后来并没有拿出我已经写的大部分内容。我可能会回到它, 或者欢迎编辑删除整个段落,如果它们真的是多余的。我认为第二次更详细地重复内容可能对一些读者有帮助,所以我把中间更冗长的部分留在了中间,但是有些可能是多余的。基本上我已经厌倦了编辑它并发布了我拥有的内容:P)
【解决方案2】:

当前使用 gcc 11.3.1 或更高版本。 我不是程序员,但区分 C 和 C++。 三年来,我一直在 github/doom9 论坛上制作最新的编解码器。 在我的旧 Intel (R) Core (TM) i5-2500K CPU @ 3.30GHz 上,我注意到了这一点。 在 C 语言中,您可以播放 SIMD AVX2 ex。用于非 SIMD 处理器的汇编器编解码器。我们可以使用论坛上发布的编解码器吗?谁知道呢。前任。 libjpeg,dav1d,带 SIMD,不带 mavx2。

xeve、xevd、uvg266、uavs3e、uavs3d、aom、libavif

在 C++ SIMD AVX2 中,您甚至不会打开帮助。 第二件事是线程和 Unix 与 Windows 的兼容性。 在 C 中,这比在 C++ 中更快。同样在 C++ 中,您必须向 g++ 添加一些未经测试的特殊添加,例如 mingw-std-thread 以使一切正常。 另一个对 C++ 的好奇。 MSYS2 GCC 12.1.0。用 AVX2/AVX3 制作的编解码器在旧处理器上打开。它是如何制作的?我不知道,但不是上面的功能。

jpegxl、libwebp2、libheif、jvetvvc、vvenc、vvdec、libraw、jpegls、jpegxt、openhtj2k、openjph、grok(C++20 openjpeg)

【讨论】:

  • 如果 C 代码确实使用 AVX2 指令,它不会在像 i5 2500K 这样的 Sandy Bridge CPU 上运行。 C 和 C++ 之间的工作方式没有一般的区别,也许只是在您正在构建的代码中碰巧没有实际使用任何 AVX2 指令。大多数带有手写汇编的视频编解码器(如 x264 / x265)会根据 CPU 检测进行运行时调度,以避免运行当前 CPU 不支持的任何指令。
  • 没有 AVX3 这样的东西。你是说AVX-512吗?无论如何,如果您更详细地说明您所做的事情,您的实践经验可能对未来的读者有用。但是您声称的事情通常不是真的,所以我认为这没有帮助。例如,godbolt.org/z/qMevsao8s 显示了一个简单的 C 程序,gcc -O3 -march=haswell 编译该程序以使用 AVX2 指令。它可以优化掉(常数的阶乘,带有包装,所以答案可能总是为零)但 gcc 和 clang 碰巧没有。
猜你喜欢
  • 1970-01-01
  • 2020-07-24
  • 1970-01-01
  • 2011-05-03
  • 2011-02-23
  • 2017-05-20
  • 2019-01-31
  • 1970-01-01
  • 2015-03-03
相关资源
最近更新 更多