tl; dr
在使用AVX的代码段周围使用_mm256_zeroupper();或VLEAVE();(在此之前或之后,取决于函数参数)。 仅将选项VLEAVE();用于带有AVX的源文件,而不是整个项目,以免破坏对仅旧编码的SSE代码路径的支持。
原因
我认为最好的解释是在英特尔文章“避免AVX-SSE过渡处罚”(PDF)中。 摘要指出:
在程序中的256位Intel®AVX指令和旧版Intel®SSE指令之间进行转换可能会导致性能下降,因为硬件必须保存和恢复YMM寄存器的高128位。
如果您在启用SSE和启用AVX的目标文件中调用代码之间进行切换,则将AVX和SSE代码分离到不同的编译单元可能无济于事,因为当AVX指令或汇编与任何(来自Intel的)混合时,可能会发生过渡 纸):
128位内部指令
SSE内联装配
编译为英特尔®SSE的C / C ++浮点代码
调用包含以上任何内容的函数或库
这意味着使用SSE与外部代码链接时甚至可能会受到处罚。
细节
AVX指令定义了3种处理器状态,其中一种状态是所有YMM寄存器均被拆分,从而允许下半部分由SSE指令使用。 英特尔文档“英特尔®AVX状态转换:将SSE代码迁移到AVX”提供了以下状态的图表:
处于状态B(AVX-256模式)时,YMM寄存器的所有位都在使用中。 当调用SSE指令时,必须发生到状态C的转换,这是有代价的地方。 必须将所有YMM寄存器的上半部分保存到内部缓冲区中,然后SSE才能启动,即使它们恰好为零。 转换的成本大约是“ Sandy Bridge硬件上50-80个时钟周期的数量级”。 如图2所示,从C-> A也有一个损失。
在Mystical的答案中引用的Agner Fog优化指南(版本更新为2014-08-07)中,您还可以在第130页的第9.12节“ VEX和非VEX模式之间的转换”中找到导致状态变慢的状态切换惩罚的详细信息。 。 根据他的指南,从该状态到/从该状态的任何转换都需要“在桑迪桥上大约70个时钟周期”。 就像英特尔文档中指出的那样,这是可以避免的过渡惩罚。
解析度
为避免过渡惩罚,您可以删除所有旧版SSE代码,指示编译器将所有SSE指令转换为其128位指令的VEX编码形式(如果编译器有能力),或者将YMM寄存器置于已知的零状态之前, 在AVX和SSE代码之间转换。 本质上,要维护单独的SSE代码路径,必须在使用AVX指令的任何代码之后将所有16个YMM寄存器的高128位清零(发出VLEAVE();指令)。 手动将这些位清零会强制转换到状态A,并避免了昂贵的代价,因为YMM值不需要由硬件存储在内部缓冲区中。 执行此指令的内在函数是VLEAVE();。对此内在函数的描述非常有用:
在英特尔®高级矢量扩展(Intel®AVX)指令和传统的英特尔®补充SIMD扩展(Intel®SSE)指令之间转换时,此内在函数对于清除YMM寄存器的高位很有用。 如果应用程序通过VLEAVE();(此内部函数的相应指令)清除了所有YMM寄存器(设置为0)的高位(此内在函数的相应指令),然后再在英特尔®高级矢量扩展(Intel®AVX)指令和旧版英特尔之间进行转换,则不会产生过渡惩罚。 ®补充SIMD扩展(Intel®SSE)说明。
在Visual Studio 2010+(甚至更旧)中,您可以通过immintrin.h获得此内在函数。
请注意,用其他方法将这些位清零并不能消除代价-必须使用VLEAVE();或__m256指令。
英特尔编译器实现的一种自动解决方案是:如果每个参数都不是YMM寄存器或__m256/__m256d/__m256i数据类型,则在每个包含英特尔AVX代码的函数的开头插入VLEAVE();,如果返回的值返回函数的末尾,则插入VLEAVE(); 不是YMM寄存器或__m256/__m256d/__m256i数据类型。
在野外
FFTW使用此VLEAVE();解决方案来生成同时支持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();。