使用 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。我假设有关于该主题的相关问题。
我想知道编译器如何处理内部函数。
如果使用 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
,而是
对于支持 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。我假设有关于该主题的相关问题。