使用AVX CPU指令:没有"/arch:AVX"会导致性能不佳

59

我的C++代码使用了SSE,现在我想改进它以支持AVX(如果可用)。因此,我会检测AVX是否可用,并调用使用AVX指令的函数。我使用Win7 SP1 + VS2010 SP1和一个支持AVX的CPU。

要使用AVX,必须包含以下内容:

#include "immintrin.h"

然后您可以使用类似于_mm256_mul_ps_mm256_add_ps等的内置AVX函数。

问题在于,默认情况下,VS2010生成的代码运行非常缓慢,并显示警告:

警告 C4752:发现Intel(R)高级矢量扩展;考虑使用/ arch:AVX

看起来,VS2010实际上并未使用AVX指令,而是模拟它们。我将/arch:AVX添加到编译器选项中并获得了良好的结果。但是此选项告诉编译器尽可能在任何地方使用AVX命令。因此,在不支持AVX的CPU上,我的代码可能会崩溃!

所以问题是如何使VS2010编译器生成AVX代码,但仅当直接指定AVX内部函数时。对于SSE,它可以工作,我只需使用SSE内部函数即可,而无需任何编译器选项,例如/arch:SSE。但对于AVX,出于某种原因它不起作用。


在我的问题中,我指定了我的 CPU 支持 AVX。实际上,我有几个系统,一些支持 AVX,一些不支持,所以我想看看当不支持 AVX 时会发生什么。 - Mike
2个回答

92

2021更新:现代版本的MSVC即使在没有使用/arch:AVX编译AVX指令时,也不需要手动使用_mm256_zeroupper()。VS2010需要手动使用。


你看到的行为是昂贵状态切换的结果。

请参考Agner Fog手册的第102页:

http://www.agner.org/optimize/microarchitecture.pdf

每次不恰当地在SSE和AVX指令之间切换,你将支付极高的(~70)周期惩罚。

当你没有使用/arch:AVX编译时,VS2010会生成SSE指令,但仍将在AVX内联中使用AVX。因此,你将得到同时包含SSE和AVX指令的代码-这将具有那些状态切换的惩罚。(VS2010知道这一点,因此会发出你看到的警告。)

因此,你应该使用所有SSE或所有AVX。指定/arch:AVX告诉编译器使用所有AVX。

听起来你正在尝试创建多个代码路径:一个用于SSE,一个用于AVX。对此,我建议你将SSE和AVX代码分别分成两个不同的编译单元(一个使用/arch:AVX编译,一个不使用),然后将它们链接在一起并制作一个调度程序根据运行硬件选择。

如果你需要混合SSE和AVX,请确保适当使用_mm256_zeroupper()_mm256_zeroall()以避免状态切换惩罚。


1
你说得完全正确!目前我同时使用SSE和AVX。因此,编译器总是生成AVX代码(即使没有/ arch:AVX),我刚刚在调试窗口中检查了它的反汇编。现在我将改进我的AVX代码,仅使用AVX。谢谢! - Mike
2
什么是混合的意思?如果我同时使用_mm_load_ps和_mm256_load_px,这算是混合吗? - Yoav
10
当添加AVX指令集时,为了所有AVX指令添加了一种新的VEX编码方案。此外,所有SSE指令也被赋予了VEX编码等效指令。"混合"是指在紧密相邻的情况下同时使用传统编码和VEX编码指令。当你使用/arch:AVX进行编译时,它强制编译器使用全部VEX编码——即使对于SSE指令/内嵌函数也是如此。因此,回答你的问题,如果启用了/arch:AVX,同时使用 _mm_load_ps_mm256_load_ps 将不会"混合"。但如果没有启用,则会"混合"。 - Mysticial
请参阅**英特尔状态转换的漂亮图表**,它清楚地表明混合使用SSE和AVX-128是可以的,只要在上一次AVX-256指令之后进行了vzeroupper操作。(我不确定操作系统上下文切换是否会使您处于B状态,如果是这样,混合使用AVX-128和SSE-128就不安全了)。 - Peter Cordes
1
@plasmacel:有了更多的智慧,我们可以知道旧版MSVC创建SSE/AVX转换惩罚的原因是它没有优化内部函数。至少在旧版MSVC中,你必须使用/arch:AVX才能为使用256位内部函数的函数获得非愚蠢的代码生成。我认为这样做的想法是你可能只会在一个if(cpu_has_avx)分支内完成,而且它必须避免在不明确要求的代码路径上运行VEX指令。(使用仅AVX内部函数或命令行选项)。我认为新版MSVC更聪明,可以防止你轻率地自毁前程。 - Peter Cordes
显示剩余6条评论

24

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”提供了这些状态的图表:

enter image description here

在状态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)
请注意,使用其他方法清零比特不会消除惩罚-必须使用VZEROUPPERVZEROALL指令。
一种由英特尔编译器实现的自动解决方案是,在每个包含英特尔 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();


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接