避免 AVX-SSE (VEX) 转换惩罚

Avoiding AVX-SSE (VEX) Transition Penalties

我们的 64 位应用程序有很多代码(尤其是在标准库中)在 SSE 模式下使用 xmm0-xmm7 寄存器。

我想使用 ymm 寄存器实现快速内存复制。我不能修改所有使用 xmm 寄存器添加 VEX 前缀的代码,而且我也认为这不切实际,因为它会增加代码的大小可以使它 运行 变慢,因为需要 CPU 解码更大的指令。

我只想使用两个 ymm 寄存器(可能还有 zmm - 支持 zmm 的负担得起的处理器有望在今年上市)用于快速内存复制。

问题是:如何使用 ymm 寄存器但避免转换惩罚?

当我只使用 ymm8-ymm15 寄存器(而不是 ymm0-ymm7)时会出现惩罚吗? SSE 最初有 8 个 128 位寄存器 (xmm0-xmm7),但在 64 位模式下 (xmm8-xmm15) 也可用于非 VEX 前缀指令。但是,我查看了我们的 64 位应用程序,它只使用 xmm0-xmm7,因为它还有一个代码几乎相同的 32 位版本。是否仅当 CPU 实际上尝试使用之前用作 ymm 且高 128 位之一非零的 xmm 寄存器时才会出现惩罚?将快速内存复制后使用的 ymm 寄存器归零不是更好吗?例如,我曾使用 ymm 寄存器复制 32 个字节的内存——将它归零的最快方法是什么? "vpxor ymm15, ymm15, ymm15" 够快吗? (据我所知,vpxor 可以在 3 个 ALU 执行端口中的任何一个执行,p0/p1/p5,而 vxorpd 只能在 p5 上执行)。难道不是时候将它归零而不是使用它来复制 32 字节的内存吗?

为了避免对所有架构的惩罚,只需要在使用 VEX 编码指令的代码部分之后发出 vzeroallvzeroupper,然后再返回其余代码使用非 VEX 指令。

无论如何,发出这些指令被认为是所有使用 AVX 的例程的良好做法,而且很便宜 - 也许 Knights Landing 除外,但我怀疑您正在使用该架构。即使你是,性能特征也与 desktop/Xeon 系列有很大不同,所以你可能需要在那里单独编译。

这些是 只有 条从脏上层状态移动到干净上层状态的指令。您不能简单地将您使用过的特定寄存器归零,因为芯片不会逐个寄存器地跟踪脏状态。

这些 vzero* 指令的成本是几个周期:因此,如果您在 AVX 中所做的一切都是值得的,那么支付这一小成本通常是值得的。

我在 https://software.intel.com/en-us/forums/intel-isa-extensions/topic/704023

的英特尔论坛上找到了 Agner 感兴趣的注释

它回答了如果我只使用 ymm8-ymm9 而应用程序使用 xmm0-xmm7 会发生什么的问题,所以我们使用不同的寄存器。

这是引用。

I just made a few more experiments on a Haswell. It treats all vector registers as having a dirty upper half if just one ymm register has been touched. In other words, if you modify ymm1 then a non-VEX instruction writing to xmm2 will have a false dependense on the previous value of xmm2. Knights Landing has no such false dependence. Perhaps it is remembering the state of each register separately?

Hopefully, future Intel processors will either remember the state of each register separately, or at least treat zmm16-zmm31 separately so that they don't pollute xmm0-xmm15. Can you reveal something about this?

2016 年 12 月 28 日的回答未回复。

Agnger 的博客 http://www.agner.org/optimize/blog/read.php?i=761

上也有一些关于 VZEROUPPER 的有趣信息

最佳解决方案可能是重新编译所有带有 VEX 前缀的代码。 VEX 编码指令大多与相同指令的非 VEX 版本大小相同,因为非 VEX 指令带有大量前缀和转义码的遗留问题(由于指令中存在短视补丁的长期历史编码方案)。 VEX 前缀将所有旧前缀和转义码组合成两个或三个字节的单个前缀(AVX512 为四个字节)。

VEX/non-VEX 转换在不同的处理器上以不同的方式工作(参见 ):

较旧的 Intel 处理器:需要 VZEROUPPER 指令才能在处理器的不同内部状态之间进行干净的转换。

在 Intel Skylake 或更高版本的处理器上:需要 VZEROUPPER 来避免非 VEX 指令对寄存器上部的错误依赖。

在当前的 AMD 处理器上:一个 256 位寄存器被视为两个 128 位寄存器。不需要 VZEROUPPER,除了与 Intel 处理器的兼容性。 VZEROUPPER 的成本约为 6 个时钟周期。

在所有指令上使用 VEX 前缀的好处是可以避免所有处理器上的这些转换成本。您的遗留代码可能会受益于热最内层循环中的一些 256 位操作。

VEX 前缀的缺点是代码与旧处理器不兼容,因此您可能需要为 运行 在旧处理器

上保留旧版本

根据我的经验,Avoiding AVX-SSE (VEX) Transition Penalties 的最佳方法是让编译器使用微体系结构的本机代码。例如,您可以在 AVX-Intrinsics 旁边使用 SSE-Intrinsics 并使用 -march=native。我的 GCC 6.2 编译程序并使用 VEX-Encoded 指令。如果您看到生成的程序集,您会发现在所有 SSE 翻译代码之前有一个额外的 v。另一方面,如果您怀疑在使用 ymm 寄存器后可以在程序的每一点使用 __asm__ __volatile__ ( "vzeroupper" : : : );,但您应该小心。

另一种可能性是使用寄存器 zmm16 - zmm31。这些寄存器没有非 VEX 对应物。将 zmm16 - zmm31 与非 VEX SSE 代码混合使用没有状态转换和惩罚。这些 512 位寄存器仅在 64 位模式下可用,并且仅在具有 AVX512 的处理器上可用。