tl;dr 仅适用于旧版本的 MSVC
在使用 AVX 的代码部分周围使用 _mm256_zeroupper(); 或 _mm256_zeroall();(之前或之后取决于函数参数)。仅对带有 AVX 的源文件而不是整个项目使用选项 /arch:AVX,以避免破坏对旧版编码的仅 SSE 代码路径的支持。
在现代 MSVC(和其他主流编译器,GCC/clang/ICC)中,编译器知道何时使用 vzeroupper asm 指令。 使用内在函数强制额外的 vzerouppers 可能会造成伤害内联时的性能。见Do I need to use _mm256_zeroupper in 2021?
原因
我认为最好的解释是在英特尔文章中,"Avoiding AVX-SSE Transition Penalties" (PDF)。摘要指出:
在程序中在 256 位英特尔® AVX 指令和旧版英特尔® SSE 指令之间转换可能会导致性能下降,因为硬件必须保存和恢复 YMM 寄存器的高 128 位。
如果您在从支持 SSE 和支持 AVX 的目标文件调用代码之间切换,则将 AVX 和 SSE 代码分离到不同的编译单元可能没有帮助,因为转换可能会在 AVX 指令或程序集与任何(来自英特尔论文)混合:
- 128 位内部指令
- SSE 内联汇编
- 编译为英特尔® SSE 的 C/C++ 浮点代码
- 调用包含上述任何内容的函数或库
这意味着使用 SSE 与外部代码链接时甚至可能会受到处罚。
详情
AVX 指令定义了 3 种处理器状态,其中一种状态是所有YMM 寄存器都被拆分,允许SSE instructions 使用下半部分。英特尔文档“Intel® AVX State Transitions: Migrating SSE Code to AVX”提供了这些状态的图表:
当处于状态 B(AVX-256 模式)时,YMM 寄存器的所有位都在使用中。当调用 SSE 指令时,必须发生到状态 C 的转换,这就是有惩罚的地方。所有 YMM 寄存器的上半部分必须在 SSE 启动之前保存到内部缓冲区中,即使它们恰好为零。转换的成本是“在 Sandy Bridge 硬件上大约 50-80 个时钟周期”。还有一个从 C -> A 的惩罚,如图 2 所示。
您还可以在第 130 页,第 9.12 节,“Agner Fog's optimization guide 与非 VEX 模式之间的转换”(2014 年 8 月 7 日更新的版本)中找到有关导致此减速的状态切换惩罚的详细信息,在Mystical's answer 中引用。根据他的指南,任何与此状态的转换都需要“在 Sandy Bridge 上大约 70 个时钟周期”。正如英特尔文档所述,这是可以避免的过渡惩罚。
Skylake 有一种不同的脏上层机制,该机制会导致带有脏上层的遗留 SSE 的错误依赖,而不是一次性惩罚。 Why is this SSE code 6 times slower without VZEROUPPER on Skylake?
分辨率
为避免转换惩罚,您可以删除所有旧版 SSE 代码,指示编译器将所有 SSE 指令转换为其 VEX 编码形式的 128 位指令(如果编译器有能力),或者将 YMM 寄存器放在已知的在 AVX 和 SSE 代码之间转换之前的零状态。本质上,为了维护单独的 SSE 代码路径,您必须在任何使用 AVX 指令的代码之后将所有 16 个 YMM 寄存器的高 128 位清零(发出VZEROUPPER 指令)。手动清零这些位会强制转换到状态 A,并避免代价高昂的代价,因为 YMM 值不需要通过硬件存储在内部缓冲区中。执行这条指令的内在函数是_mm256_zeroupper。这个内在函数的描述非常有用:
在英特尔® 高级矢量扩展(英特尔® AVX)指令和旧版英特尔® 补充 SIMD 扩展(英特尔® SSE)指令之间转换时,此内在函数可用于清除 YMM 寄存器的高位。 如果应用程序在英特尔® 高级矢量扩展之间转换之前通过此内在函数的相应指令 VZEROUPPER 清除所有 YMM 寄存器的高位(设置为“0”),则不会有转换损失(英特尔® AVX)指令和旧版英特尔® 补充 SIMD 扩展(英特尔® SSE)指令。
在 Visual Studio 2010+(可能更早)中,you get this intrinsic 带有 immintrin.h。
请注意,使用其他方法将位清零并不能消除惩罚 - 必须使用 VZEROUPPER 或 VZEROALL 指令。
英特尔编译器实现的一个自动解决方案是,如果没有任何参数是 YMM 寄存器或__m256/@,则在每个包含英特尔 AVX 代码的函数的开头插入一个VZEROUPPER 987654349@/__m256i 数据类型,如果返回值不是 YMM 寄存器或 __m256/__m256d/__m256i 数据类型,则在函数的末尾。
在野外
FFTW 使用此VZEROUPPER 解决方案来生成同时支持 SSE 和 AVX 的库。见simd-avx.h:
/* Use VZEROUPPER to avoid the penalty of switching from AVX to SSE.
See Intel Optimization Manual (April 2011, version 248966), Section
11.3 */
#define VLEAVE _mm256_zeroupper
然后VLEAVE();在每个函数的末尾被调用,使用AVX指令的内在函数。