57

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

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

#include "immintrin.h"

然后你可以使用内在的 AVX 函数,比如_mm256_mul_ps_mm256_add_ps。问题是,默认情况下,VS2010 生成的代码运行速度非常慢并显示警告:

警告 C4752:找到 Intel(R) Advanced Vector Extensions;考虑使用 /arch:AVX

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

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

4

2 回答 2

90

2021 年更新:_mm256_zeroupper()即使在没有/arch:AVX. 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()适当避免状态切换惩罚。

于 2011-10-20T19:35:31.913 回答
24

tl;博士仅适用于旧版本的 MSVC

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

在现代 MSVC(和其他主流编译器,GCC/clang/ICC)中,编译器知道何时使用vzeroupperasm 指令。vzeroupper内联时 强制使用额外的 s 可能会损害性能。请参阅我是否需要在 2021 年使用 _mm256_zeroupper?

原因

我认为最好的解释是在英特尔文章“避免 AVX-SSE 过渡惩罚”PDF)中。摘要指出:

在程序中在 256 位英特尔® AVX 指令和旧版英特尔® 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 个时钟周期”。还有一个从 C -> A 的惩罚,如图 2 所示。

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

Skylake 有一种不同的脏鞋面机制,它会导致带有脏鞋面的传统 SSE 的错误依赖,而不是一次性惩罚。 为什么在 Skylake 上没有 VZEROUPPER 时,这个 SSE 代码会慢 6 倍?

解析度

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

在英特尔® 高级矢量扩展(英特尔® AVX)指令和旧版英特尔® 补充 SIMD 扩展(英特尔® SSE)指令之间转换时,此内在函数可用于清除 YMM 寄存器的高位。如果应用程序在英特尔® 高级矢量扩展(英特尔® AVX)指令和旧版英特尔® 之间转换之前通过此内在函数的相应指令清除所有 YMM 寄存器的高位(设置为“0”) ,则不会出现转换损失VZEROUPPER补充 SIMD 扩展(英特尔® SSE)指令。

在 Visual Studio 2010+(可能更早)中,您可以通过 immintrin.h获得此内在函数。

请注意,使用其他方法将位清零并不能消除惩罚 -必须使用VZEROUPPERor指令。VZEROALL

英特尔编译器实现的一种自动解决方案是,如果参数都不是 YMM 寄存器或//数据类型,则在每个包含英特尔 AVX 代码的函数的开头插入VZEROUPPER a,如果返回的值不是 YMM,则在函数的末尾插入注册或//数据类型。__m256__m256d__m256i__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 指令的内在函数调用。

于 2015-02-06T01:04:53.570 回答