使用 SSE / AVX Intriniics 时架构的影响

The Effect of Architecture When Using SSE / AVX Intrinisics

我想知道编译器如何处理内部函数。

如果使用 SSE2 Intrinsics(使用 #include <emmintrin.h>)并使用 -mavx 标志进行编译。编译器会生成什么?它会生成 AVX 或 SSE 代码吗?

如果使用 AVX2 Intrinsics(使用 #include <immintrin.h>)并使用 -msse2 标志进行编译。编译器会生成什么?它会生成 SSE Only 还是 AVX 代码?

编译器如何处理内部函数?
如果使用 Intrinsics,它是否有助于编译器理解循环中的依赖关系以实现更好的矢量化?

例如,这里发生了什么 - https://godbolt.org/z/Y4J5OA (Or https://godbolt.org/z/LZOJ2K)?
查看所有 3 个窗格。

背景

我正在尝试构建具有不同 CPU 功能(SSE4 和 AVX2)的相同功能的各种版本。
我正在使用 SSE Intrinsics 和 AVX Intrinsics 编写相同的版本。
假设他们是名字 MyFunSSE()MyFunAVX()。两者都在同一个文件中。

如何让编译器(同样的方法适用于 MSVC、GCC 和 ICC)仅使用各自的函数构建它们?

GCC 和 clang 要求您启用您使用的所有扩展。否则就是编译时错误,比如 error: inlining failed in call to always_inline ‘__m256d _mm256_mask_loadu_pd(__m256d, __mmask8, const void*)’: target specific option mismatch

使用 -march=native-march=haswell 或任何优于启用特定扩展的方法,因为这也设置了适当的调整选项。而且你不要忘记像 -mpopcnt 这样有用的东西,它会让 std::bitset::count() 内联一条 popcnt 指令,并使所有可变计数移位更有效地使用 BMI2 shlx / shrx(1 uop 对 3)


MSVC 和 ICC 没有,并且允许您使用内在函数发出它们无法自动矢量化的指令。

如果您使用 AVX 内部函数,您应该 绝对启用 AVX。未启用 AVX 的旧版 MSVC 并不总是在需要时自动使用 vzeroupper,而是 。尽管如此,如果您的整个程序都可以假定支持 AVX,那么即使对于 MSVC,也一定要告诉编译器。


对于支持 GNU 扩展(GCC、clang、ICC)的编译器,您可以在编译单元中的特定函数上使用 __attribute__((target("avx"))) 之类的东西。或者更好的是,__attribute__((target("arch=haswell"))) 也可以设置调整选项。 (这也会启用您可能不想要的 AVX2 和 FMA。而且我不确定 target 属性是否设置 -mtune=xx)。看 https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html

__attribute__((target())) 将阻止它们将 内联到具有其他目标选项的函数中,因此如果函数本身是太小。在包含循环的函数上使用它,而不是在循环中调用的辅助函数。

另请参阅 https://gcc.gnu.org/wiki/FunctionMultiVersioning 用于在 相同 函数名称的多个定义上使用不同的目标选项,用于编译器支持的运行时调度。但我不认为有一种可移植的(到 MSVC)方法来做到这一点。


对于 MSVC,您不需要任何东西,尽管就像我说的那样,我认为在没有 -arch:AVX 的情况下使用 AVX 内部函数通常不是一个好主意,因此您最好将它们放在一个单独的文件中。但是对于 AVX 与 AVX2 + FMA,或 SSE2 与 SSE4.2,你没有任何东西都很好。

只是 #define AVX2_FUNCTION 到空字符串而不是 __attribute__((target("avx2,fma")))

#if defined(__GNUC__) && !defined(__INTEL_COMPILER)
// apparently ICC doesn't support target attributes, despite supporting GNU C
#define TARGET_HASWELL __attribute__((target("arch=haswell")))
#else
#define TARGET_HASWELL   // empty
 // maybe warn if __AVX__ isn't defined for functions where this is used?
 // if you need to make sure MSVC uses vzeroupper everywhere needed.
#endif


TARGET_HASWELL
void foo_avx(float *__restrict dst, float *__restrict src)
{
   for (size_t i = 0 ; i<1024 ; i++) {
       __m256 v = _mm256_loadu_ps(src);
       ...
       ...
   }
}

使用 GCC 和 clang,宏扩展为 __attribute__((target)) 东西;对于 MSVC 和 ICC,它没有。


ICC 杂注:

https://software.intel.com/en-us/cpp-compiler-developer-guide-and-reference-optimization-parameter 记录了您希望在 AVX 函数之前放置的编译指示,以确保在使用 _mm256 内在函数的函数中正确使用 vzeroupper。

#pragma intel optimization_parameter target_arch=AVX

对于 ICC,你可以像这样 #define TARGET_AVX,并且总是在函数之前单独使用它,你可以在其中放置一个 __attribute__ 或一个 pragma。如果 ICC 不希望在声明中这样做,您可能还需要单独的宏来定义和声明函数。如果你想在它们之后有非 AVX 函数,还有一个宏来结束一个 AVX 函数块。 (对于非 ICC 编译器,这将是空的。)

如果您在启用 -mavx2 的情况下编译代码,您的编译器将(通常)生成所谓的 "VEX encoded" 指令。在 _mm_loadu_ps 的情况下,这将生成 vmovups 而不是 movups,这几乎是等价的,只是后者只会修改目标寄存器的低 128 位,而前者会将低 128 位以上的所有内容清零。但是,它只会在至少支持 AVX 的机器上 运行。 Details on [v]movups are here.

对于 [v]addps 等其他指令,AVX 具有允许三个操作数的额外优势(即目标可以不同于两个源),在某些情况下可以避免复制寄存器。例如,

_mm_mul_ps(_mm_add_ps(a,b), _mm_sub_ps(a,b));

为 SSE 编译时需要寄存器副本 (movaps),但为 AVX 编译时则不需要: https://godbolt.org/z/YHN5OA


关于使用 AVX-intrinsics 但在没有 AVX 的情况下进行编译,编译器要么失败(如 gcc/clang),要么静默生成相应的指令,然后在没有 AVX 支持的机器上失败(有关详细信息,请参阅@PeterCordes 的回答) ).


附录:如果您想根据体系结构(在编译时)实现不同的功能,您可以使用 #ifdef __AVX__#if defined(__AVX__) 进行检查:https://godbolt.org/z/ZVAo-7

我认为在同一个编译单元中实现它们很困难。最简单的解决方案是构建不同的共享库甚至不同的二进制文件,并有一个小的二进制文件来检测可用的 CPU 功能并加载相应的 library/binary。我假设有关于该主题的相关问题。