使用AVX CPU指令:没有“/ arch:AVX”

我的C ++代码使用SSE,现在我想改进它以支持AVX。 所以我检测AVX何时可用,并调用一个使用AVX命令的函数。 我使用Win7 SP1 + VS2010 SP1和一个带有AVX的CPU。

要使用AVX,有必要包含这一点:

#include "immintrin.h" 

然后你可以使用像_mm256_mul_ps函数,如_mm256_mul_ps_mm256_add_ps等。问题是,默认情况下,VS2010产生的代码工作非常缓慢,并显示警告:

警告C4752:find英特尔(R)高级vector扩展; 考虑使用/ arch:AVX

看来VS2010实际上并不使用AVX指令,而是模拟它们。 我添加/arch:AVX的编译器选项,并取得了良好的效果。 但是这个选项告诉编译器在可能的地方使用AVX命令。 所以我的代码可能会在不支持AVX的CPU上崩溃!

所以问题是如何使VS2010编译器生成AVX代码,但只有当我直接指定AVX内部函数。 对于SSE它可以工作,我只是使用SSE内在函数,它产生的SSE代码没有像/arch:SSE这样的编译器选项。 但是对于AVX来说,由于某种原因它不起作用。

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

见Agner雾的手册的第102页:

optimize/microarchitecture.pdf

每当您在SSE和AVX指令之间来回切换时,您将会付出极高(〜70)的周期处罚。

当你编译没有/arch:AVX ,VS2010会生成SSE指令,但是只要你有AVX内在函数,仍然会使用AVX。 因此,你会得到既有SSE又有AVX指令的代码 – 这些指令会有这些状态切换的惩罚。 (VS2010知道这一点,所以它发出了你所看到的警告。)

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

这听起来像是你想要制作多个代码path:一个是SSE,一个是AVX。 为此,我build议你将SSE和AVX代码分成两个不同的编译单元。 (一个用/arch:AVX编译,一个不用)然后将它们链接在一起,并使调度员根据正在运行的硬件进行select。

如果您需要混合使用SSE和AVX,请确保正确使用_mm256_zeroupper()_mm256_zeroall()以避免状态切换损失。

TL;博士

使用_mm256_zeroupper();_mm256_zeroall(); 围绕使用AVX的代码部分(取决于函数参数之前或之后)。 仅对/arch:AVX源文件使用选项/arch:AVX而不是整个项目,以避免打破对传统编码SSE专用代码path的支持。

原因

我认为最好的解释是英特尔文章“避免AVX-SSE转换处罚” ( PDF )。 摘要说明:

程序中的256位英特尔®AVX指令和传统英特尔®SSE指令之间的转换可能会导致性能损失,因为硬件必须保存并恢复YMM寄存器的高128位。

将AVX和SSE代码分离为不同的编译单元可能无助于在启用了SSE和启用AVX的目标文件中调用代码,因为当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个时钟周期”。 从C – > A也有一个惩罚,如图2所示。

您也可以在Agner Fog的优化指南 (版本2014-08-07更新版)中find第130页第9.12节“在VEX和非VEX模式之间转换”的状态切换损失的详细信息,在Mystical的答案中引用。 根据他的指导,任何到这个状态的转换都需要“在Sandy Bridge上大约70个时钟周期”。 正如英特尔文件所述,这是一个可以避免的过渡惩罚。

parsing度

为避免转换惩罚,您可以删除所有传统的SSE代码,指示编译器将所有SSE指令转换为其128位指令的VEX编码forms(如果编译器有能力),或者将YMM寄存器置于已知的零状态在AVX和SSE代码之间转换。 实质上,为了保持单独的SSE代码path,必须在使用AVX指令的任何代码之后,将所有16个YMM寄存器的高128位(发出VZEROUPPER指令) VZEROUPPER 。 手动清零这些位会强制转换到状态A,并且避免了昂贵的代价,因为YMM值不需要通过硬件存储在内部缓冲区中。 执行这个指令的内在是_mm256_zeroupper 。 这个内在的描述是非常丰富的:

当在英特尔®高级vector扩展(英特尔®AVX)指令和传统英特尔®补充SIMD扩展指令(英特尔®SSE)之间转换时,此固有特性有助于清除YMM寄存器的高位。 如果在英特尔®高级vector扩展(英特尔®AVX)指令和传统英特尔之间转换之前, 应用程序通过VZEROUPPER (对应于该内部指令) 清除所有YMM寄存器的高位 (设为0) ®补充SIMD扩展(英特尔®SSE)指令。

在Visual Studio 2010+中(甚至可能更老), 你可以用immintrin.h来获得这个内在的东西。

请注意,用其他方法清零这些位并不能消除惩罚 – 必须使用VZEROUPPERVZEROALL指令。

英特尔编译器实现的一个自动解决scheme是每个包含英特尔AVX代码的函数的开始处插入一个VZEROUPPER 如果没有参数是YMM寄存器或__m256 / __m256d / __m256i数据types,并且函数结束时返回值不是YMM寄存器或__m256 / __m256d / __m256i数据types。

在野外

这个VZEROUPPER解决scheme被FFTW用来生成一个同时支持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指令的内在函数的函数结束时被调用。