【问题标题】:Using AVX CPU instructions: Poor performance without "/arch:AVX"使用 AVX CPU 指令:没有“/arch:AVX”的性能很差
【发布时间】:2011-12-12 00:00:25
【问题描述】:

我的 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) 高级矢量扩展;考虑 使用 /arch:AVX

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

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

【问题讨论】:

  • 在我的问题中,我指定我的 CPU 支持 AVX。实际上我有几个系统,一些有 AVX,有一些没有,所以我看看不支持 AVX 时会发生什么。

标签: c++ performance visual-studio-2010 sse avx


【解决方案1】:

2021 年更新:即使在没有 /arch:AVX 的情况下编译 AVX 内部函数时,现代版本的 MSVC 也不需要手动使用 _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() 以避免状态切换惩罚。

【讨论】:

  • 你说的完全正确!目前我同时使用 SSE 和 AVX。所以编译器总是产生 AVX 代码(即使没有 /arch:AVX),我刚刚用反汇编 dbg 窗口检查了它。现在我将改进我的 AVX 代码以仅使用 AVX。谢谢!!!
  • 更新:自从我发布此答案以来,Agner Fog 的手册似乎已经更新。相关部分现在位于第 103 页,第 8.12 节。
  • 混音是什么意思?如果我同时使用 _mm_load_ps 和 _mm256_load_px,它是否被视为混合使用?
  • @Ben-Uri 添加 AVX 后,他们为所有 AVX 指令添加了新的 VEX 编码方案。此外,所有 SSE 指令都被赋予了 VEX 编码的等价物。通过“混合”,我指的是非常接近地使用传统编码和 VEX 编码指令。当您使用 /arch:AVX 编译时,它会强制编译器使用所有 VEX 编码 - 即使对于 SSE 指令/内在函数也是如此。因此,要回答您的问题,如果您启用了 /arch:AVX,则同时使用 _mm_load_ps_mm256_load_ps 将不会“混合”。但如果你不这样做,它们混在一起。
  • @placel:得益于更多智慧:旧的 MSVC 创建 SSE/AVX 转换惩罚的原因是它没有优化内在函数。至少对于较旧的 MSVC,您必须使用 /arch:AVX 为使用 256 位内在函数的函数获取非愚蠢的代码生成。我认为这个想法是您可能只在if(cpu_has_avx) 分支内执行此操作,并且它必须避免在未明确要求它们的代码路径上运行 VEX 指令。 (仅使用 AVX 内在函数或使用命令行选项)。我认为较新的 MSVC 不那么愚蠢,并且可以阻止您天真地朝自己的脚开枪。
【解决方案2】:

tl;dr 仅适用于旧版本的 MSVC

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

在现代 MSVC(和其他主流编译器,GCC/clang/ICC)中,编译器知道何时使用 vzeroupper asm 指令。 使用内在函数强制额外的 vzerouppers 可能会造成伤害内联时的性能。见Do I need to use _mm256_zeroupper in 2021?

原因

我认为最好的解释是在英特尔文章中,"Avoiding AVX-SSE Transition Penalties" (PDF)。摘要指出:

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

如果您在从支持 SSE 和支持 AVX 的目标文件调用代码之间切换,则将 AVX 和 SSE 代码分离到不同的编译单元可能没有帮助,因为转换可能会在 AVX 指令或程序集与任何(来自英特尔论文)混合:

  • 128 位内部指令
  • SSE 内联汇编
  • 编译为英特尔® SSE 的 C/C++ 浮点代码
  • 调用包含上述任何内容的函数或库

这意味着使用 SSE 与外部代码链接时甚至可能会受到处罚。

详情

AVX 指令定义了 3 种处理器状态,其中一种状态是所有YMM 寄存器都被拆分,允许SSE instructions 使用下半部分。英特尔文档“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 节,“Agner Fog's optimization guide 与非 VEX 模式之间的转换”(2014 年 8 月 7 日更新的版本)中找到有关导致此减速的状态切换惩罚的详细信息,在Mystical's answer 中引用。根据他的指南,任何与此状态的转换都需要“在 Sandy Bridge 上大约 70 个时钟周期”。正如英特尔文档所述,这是可以避免的过渡惩罚。

Skylake 有一种不同的脏上层机制,该机制会导致带有脏上层的遗留 SSE 的错误依赖,而不是一次性惩罚。 Why is this SSE code 6 times slower without VZEROUPPER on Skylake?

分辨率

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

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

在 Visual Studio 2010+(可能更早)中,you get this intrinsic 带有 immintrin.h。

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

英特尔编译器实现的一个自动解决方案是,如果没有任何参数是 YMM 寄存器或__m256/@,则在每个包含英特尔 AVX 代码的函数的开头插入一个VZEROUPPER 987654349@/__m256i 数据类型,如果返回值不是 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

然后VLEAVE();每个函数的末尾被调用,使用AVX指令的内在函数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-06-26
    • 1970-01-01
    • 2019-05-12
    • 1970-01-01
    • 2014-08-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多