tl;dr 仅适用于旧版本的 MSVC
在使用 AVX 的代码段周围(取决于函数参数),请使用 _mm256_zeroupper();
或 _mm256_zeroall();
。仅在具有 AVX 的源文件中使用选项 /arch:AVX
,而不是整个项目,以避免破坏对遗留编码的 SSE-only 代码路径的支持。
在现代 MSVC(以及其他主流编译器,如 GCC/clang/ICC)中,编译器知道何时使用 vzeroupper
asm 指令。 使用内部函数强制额外的 vzeroupper
可能会影响性能。详情请参阅2021 年是否需要使用 _mm256_zeroupper?
原因
我认为最好的解释在英特尔文章中,
"避免AVX-SSE转换惩罚" (
PDF)。摘要如下:
在程序中在256位Intel® AVX指令和传统的Intel® SSE指令之间进行转换可能会导致性能损失,因为硬件必须保存和恢复YMM寄存器的上128位。
如果您从SSE启用和AVX启用的对象文件中调用代码并切换它们,则将AVX和SSE代码分开到不同的编译单元中
可能无法帮助,因为转换可能会在AVX指令或汇编与任何以下混合时发生(来自英特尔论文):
- 128位内在指令
- SSE内联汇编
- C/C++浮点代码,编译为Intel® SSE
- 调用包含上述任何内容的函数或库
这意味着即使使用SSE链接到外部代码也可能会受到惩罚。
细节
AVX指令定义了3种处理器状态,其中一种状态是将所有YMM寄存器分裂,允许下半部分被SSE指令使用。Intel文档“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节“
VEX和非VEX模式之间的转换”中找到有关导致此减速的状态切换惩罚的详细信息,该节在
Agner Fog的优化指南(2014-08-07版本更新)中引用了
Mystical's answer。根据他的指南,任何转换到/从此状态的时间都需要“在Sandy Bridge上约70个时钟周期”。正如Intel文档所述,这是一种可避免的转换惩罚。
Skylake有一种不同的脏上限机制,会对具有脏上限的传统SSE代码造成错误依赖,而不是一次性惩罚。为什么在Skylake上没有使用VZEROUPPER时这个SSE代码要慢6倍?
解决方法
为了避免转换惩罚,您可以删除所有旧版SSE代码,指示编译器将所有SSE指令转换为其128位VEX编码形式的指令(如果编译器能够),或在AVX和SSE代码之间转换之前将YMM寄存器置于已知的零状态。基本上,要维护单独的SSE代码路径,您必须在使用AVX指令的任何代码之后
清除所有16个YMM寄存器的高128位(发出VZEROUPPER
指令)。手动清零这些位强制转换到状态A,并避免昂贵的惩罚,因为硬件不需要将YMM值存储在内部缓冲区中。执行此指令的内在函数是
_mm256_zeroupper
。该内在函数的描述非常详细:
这个内在函数在从Intel®高级矢量扩展(Intel® AVX)指令转换为遗留的Intel®补充SIMD扩展(Intel® SSE)指令时,清除YMM寄存器的上位比特非常有用。如果应用程序通过使用对应于此内在函数的
VZEROUPPER
将所有YMM寄存器的上位比特(设置为“0”)清除,然后在Intel®高级矢量扩展(Intel® AVX)指令和遗留的Intel®补充SIMD扩展(Intel® SSE)指令之间进行转换,则没有转换惩罚。在Visual Studio 2010+(甚至更早版本),您可以通过immintrin.h获得此内在函数
(链接1)。
请注意,使用其他方法清零比特不会消除惩罚-必须使用
VZEROUPPER
或
VZEROALL
指令。
一种由英特尔编译器实现的自动解决方案是,在每个包含英特尔 AVX 代码的函数
开头插入一个
VZEROUPPER
,如果没有参数是 YMM 寄存器或
__m256
/
__m256d
/
__m256i
数据类型,并在函数
结尾插入一个
VZEROUPPER
,如果返回值不是 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
在使用AVX指令的内部函数结束时,每个函数都会调用VLEAVE();
。