在 Knights Landing 上清除一个或几个 ZMM 寄存器的最有效方法是什么?

What is the most efficient way to clear a single or a few ZMM registers on Knights Landing?

说,我想清除 4 zmm 个寄存器。

下面的代码能提供最快的速度吗?

vpxorq  zmm0, zmm0, zmm0
vpxorq  zmm1, zmm1, zmm1
vpxorq  zmm2, zmm2, zmm2
vpxorq  zmm3, zmm3, zmm3

在 AVX2 上,如果我想清除 ymm 寄存器,vpxor 最快,比 vxorps 快,因为 vpxor 可以 运行单位。

在 AVX512 上,zmm 寄存器没有 vpxor,只有 vpxorqvpxord。这是清除寄存器的有效方法吗?当我用 vpxorq 清除它们时,CPU 是否足够聪明,不会对 zmm 寄存器的先前值产生错误依赖?

我还没有物理 AVX512 CPU 来测试 - 也许有人已经在 Knights Landing 上测试过?是否有任何延迟发布

我整理了一个 simple C test program using intrinsics and compiled with ICC 17 - 我得到的用于将 4 个 zmm 寄存器(在 -O3)置零的生成代码是:

    vpxord    %zmm3, %zmm3, %zmm3                           #7.21
    vmovaps   %zmm3, %zmm2                                  #8.21
    vmovaps   %zmm3, %zmm1                                  #9.21
    vmovaps   %zmm3, %zmm0                                  #10.21

按照 Paul R 关于查看编译器生成的代码的建议,我们看到 ICC 使用 VPXORD 将一个 ZMM 寄存器置零,然后 VMOVAPS 将这个置零的 XMM 寄存器复制到任何需要清零的额外寄存器。换句话说:

vpxord    zmm3, zmm3, zmm3
vmovaps   zmm2, zmm3
vmovaps   zmm1, zmm3
vmovaps   zmm0, zmm3

GCC 本质上做同样的事情,但使用 VMOVDQA64 进行 ZMM-ZMM 寄存器移动:

vpxord      zmm3, zmm3, zmm3
vmovdqa64   zmm2, zmm3
vmovdqa64   zmm1, zmm3
vmovdqa64   zmm0, zmm3

GCC 还尝试在 VPXORDVMOVDQA64 之间安排其他指令。 ICC 没有表现出这种偏好。

Clang 使用 VPXORD 将所有 ZMM 寄存器独立清零,a la:

vpxord  zmm0, zmm0, zmm0
vpxord  zmm1, zmm1, zmm1
vpxord  zmm2, zmm2, zmm2
vpxord  zmm3, zmm3, zmm3

支持生成 AVX-512 指令的指定编译器的所有版本都遵循上述策略,并且似乎不受针对特定微体系结构的调整请求的影响。


这非常强烈地表明 VPXORD 是您应该用来清除 512 位 ZMM 寄存器的指令。

为什么 VPXORD 而不是 VPXORQ?好吧,你只关心屏蔽时的大小差异,所以如果你只是将寄存器归零,那真的没关系。两者都是 6 字节指令,根据 Agner Fog's instruction tables,在 Knights Landing 上:

  • 两者在相同数量的端口(FP0 或 FP1)上执行,
  • 两者都解码为 1 µop
  • 两者的最小延迟均为 2,相互吞吐量均为 0.5。
    (请注意,最后一个要点突出了 KNL 的一个主要缺点——所有向量指令都有至少 2 个时钟周期的延迟,即使是在其他微体系结构上具有 1 个周期延迟的简单指令也是如此。)

没有明确的赢家,但编译器似乎更喜欢 VPXORD,所以我也坚持使用那个。

VPXORD/VPXORQVXORPS/VXORPD 相比如何?好吧,正如您在问题中提到的那样,打包整数指令通常可以在比浮点指令更多的端口上执行,至少在 Intel CPU 上是这样,这使得前者更可取。然而,骑士登陆的情况并非如此。无论是压缩整数还是浮点数,所有逻辑指令都可以在 FP0 或 FP1 上执行,并且具有相同的延迟和吞吐量,因此理论上您应该能够使用其中任何一个。此外,由于两种形式的指令都在浮点单元上执行,因此混合它们不会造成跨域惩罚(转发延迟)like you would see on other microarchitectures。我的判决?坚持使用整数形式。这不是对 KNL 的悲观,而是对其他架构进行优化时的胜利,所以要保持一致。你需要记住的更少。优化本身就够难的了。

顺便说一句,在VMOVAPSVMOVDQA64之间做出决定时也是如此。它们都是 6 字节指令,它们都具有相同的延迟和吞吐量,它们都在相同的端口上执行,并且没有您必须关注的旁路延迟。出于所有实际目的,在以骑士登陆为目标时,这些可以被视为等效。

最后,你问是否"the CPU [is] smart enough not to make false dependencies on the previous values of the ZMM registers when [you] clear them with VPXORD/VPXORQ"。好吧,我不确定,但我想是的。将寄存器与自身进行异或运算以清除它已经成为 时间的既定习语,并且众所周知其他 Intel CPU 可以识别它,所以我无法想象为什么它不会'不要在 KNL 上。但即使不是,这仍然是清除寄存器的最佳方式。

备选方案类似于从内存中移入 0 值,这不仅是一个相当长的编码指令,而且还需要您支付内存访问惩罚。这不会是一场胜利……除非 可能 你是吞吐量限制的,因为 VMOVAPS 和内存操作数在不同的单元上执行(一个专用的内存单元,而不是比任何一个浮点单位)。不过,您需要一个非常引人注目的基准来证明这种优化决策的合理性。这当然不是 "general purpose" 策略。

或者你可以用它自己做一个减法?但我怀疑这比 XOR 更可能被认为是无依赖性的,并且关于执行特征的其他所有内容都是相同的,因此这不是打破标准习语的令人信服的理由。

在这两种情况下,实用性因素都发挥了作用。到了紧要关头,you have to write code for other humans to read and maintain。因为它会导致每个人在阅读您的代码后永远陷入困境,所以您最好有一个真正令人信服的理由来做一些奇怪的事情。


下一个问题:我们应该重复发出 VPXORD 指令,还是应该将一个置零寄存器复制到其他寄存器中?

嗯,VPXORDVMOVAPS 具有相同的延迟和吞吐量,解码为相同数量的微操作,并且可以在相同数量的端口上执行。从这个角度来说,没关系。

数据依赖性呢?天真地,人们可能会假设重复异或运算更好,因为移动取决于初始异或运算。也许这就是为什么 Clang 更喜欢重复 XORing,以及为什么 GCC 更喜欢在 XOR 和 MOV 之间安排其他指令。如果我在不做任何研究的情况下快速编写代码,我可能会按照 Clang 的方式编写代码。但我不能确定这是否是 most 没有基准的最佳方法。由于我们都无法使用 Knights Landing 处理器,因此获得这些并不容易。 :-)

Intel 的 Software Developer Emulator does support AVX-512, but it's unclear whether this is a cycle-exact simulator that would be suitable for benchmarking/optimization decisions. This document simultaneously suggests both that it is ("Intel SDE is useful for performance analysis, compiler development tuning, and application development of libraries.") and that it is not ("Please note that Intel SDE is a software emulator and is mainly used for emulating future instructions. It is not cycle accurate and can be very slow (up-to 100x). It is not a performance-accurate emulator."). What we need is a version of IACA 支持 Knights Landing,但遗憾的是,它还没有出现。


总而言之,很高兴看到三个最流行的编译器即使对于这样的新架构也能生成高质量、高效的代码。他们做出的偏好指令的决定略有不同,但这几乎没有实际差异。

在很多方面,我们已经看到这是因为 Knights Landing 微架构的独特之处。特别是,大多数矢量指令在两个浮点单元中的任何一个上执行,并且它们具有相同的延迟和吞吐量,这意味着没有您需要关注的域交叉惩罚,并且您没有与浮点指令相比,更喜欢压缩整数指令的特别好处。你可以在核心图中看到这一点(左边的橙色块是两个向量单元):

使用您最喜欢的指令序列。

最有效的方法是利用 AVX 隐式清零到 VLMAX(最大向量寄存器宽度,由 XCR0 的当前值决定):

vpxor  xmm6, xmm6, xmm6
vpxor  xmm7, xmm7, xmm7
vpxor  xmm8, xmm0, xmm0   # still a 2-byte VEX prefix as long as the source regs are in the low 8
vpxor  xmm9, xmm0, xmm0

这些只是 4 字节指令(2 字节 VEX 前缀),而不是 6 字节(4 字节 EVEX 前缀)。注意在低 8 位中使用源寄存器以允许 2 字节 VEX,即使目标是 xmm8-xmm15。 (当第二个源 reg 为 x/ymm8-15 时,需要 3 字节的 VEX 前缀)。是的,只要两个源操作数是相同的寄存器,这仍然被认为是归零惯用语(我测试它不使用 Skylake 上的执行单元)。

除了代码大小的影响外,在 Skylake-AVX512 和 KNL 上的性能与 vpxord/q zmmvxorps zmm 相同。 (而且越小的代码几乎总是越好。)但是请注意,KNL 的前端非常薄弱,其中最大解码吞吐量只能勉强使向量执行单元饱和,并且根据 Agner Fog's microarch guide 通常是瓶颈。 (它没有 uop 缓存或循环缓冲区,每个时钟的最大吞吐量为 2 条指令。此外,平均提取吞吐量限制为每个周期 16B。)

此外,在假设的未来 AMD(或可能是英特尔)CPUs 上将 AVX512 指令解码为两个 256b 微指令(或四个 128b 微指令),这效率更高。 so this is a real thing. Old compiler versions got it wrong (gcc bug 80636, clang bug 32862),但那些未优化的错误已在当前版本中修复(GCC8、clang6.0、MSVC since forever(?)。ICC 仍然不是最佳的。)


zmm16-31 归零确实需要 EVEX 编码指令vpxordvpxorq 同样是不错的选择。 EVEX vxorps requires AVX512DQ for some reason (unavailable on KNL), but EVEX vpxord/q 是基线 AVX512F。

vpxor   xmm14, xmm0, xmm0
vpxor   xmm15, xmm0, xmm0
vpxord  zmm16, zmm16, zmm16     # or XMM if you already use AVX512VL for anything
vpxord  zmm17, zmm17, zmm17

EVEX 前缀是固定宽度的,因此使用 zmm0 没有任何好处。

如果目标支持 AVX512VL(Skylake-AVX512 但不支持 KNL),那么您仍然可以使用 vpxord xmm31, ... 在未来的 CPU 中获得更好的性能,将 512b 指令解码为多个 uops。

如果您的目标有 AVX512DQ(Skylake-AVX512 但不是 KNL),在为 FP 数学指令创建输入时使用 vxorps 或在任何其他指令中使用 vpxord 可能是个好主意案件。对 Skylake 没有影响,但未来 CPU 可能会关心。如果始终只使用 vpxord.

更容易,请不要担心这一点

相关:在 zmm 寄存器 中生成全 1 的最佳方式似乎是 vpternlogd zmm0,zmm0,zmm0, 0xff。 (通过查找 -table 的全一,逻辑 table 中的每个条目都是 1)。 vpcmpeqd same,same 不起作用,因为 AVX512 版本比较的是掩码寄存器,而不是向量。

这个 vpternlogd/q 的特例不是独立于 KNL 或 Skylake-AVX512 的特例,所以尽量选择一个冷寄存器。不过,它在 SKL-avx512 上非常快:根据我的测试,每个时钟吞吐量 2 个。 (如果你需要多个 regs of all-ones,在 vpternlogd 上使用并复制结果,特别是如果你的代码将 运行 on Skylake 而不仅仅是 KNL)。


我选择了 32 位元素大小(vpxord 而不是 vpxorq),因为 32 位元素大小被广泛使用,如果一个元素大小要变慢,通常不会32 位很慢。例如pcmpeqq xmm0,xmm0 在 Silvermont 上比 pcmpeqd xmm0,xmm0 慢很多。 pcmpeqw 是另一种生成全一向量的方法(AVX512 之前),但 gcc 选择 pcmpeqd。我很确定它永远不会对异或归零产生影响,尤其是在没有掩码寄存器的情况下,但是如果您正在寻找选择 vpxordvpxorq 之一的理由,这是一个很好的理由,除非有人在任何 AVX512 硬件上发现真正的性能差异。

有趣的是 gcc 选择 vpxord,但 vmovdqa64 而不是 vmovdqa32


,包括 Skylake-AVX512。 (TODO:将其中一些内容合并到该答案中,并对其进行一些其他更新...)

但是在 KNL 上,我很确定异或归零需要一个执行端口。两个向量执行单元通常可以跟上前端,因此在 issue/rename 阶段处理异或归零在大多数情况下不会产生性能差异。根据 Agner Fog 的测试,vmovdqa64 / vmovaps 需要一个端口(更重要的是具有非零延迟),因此我们知道它无法处理 issue/rename 阶段的端口。 (它可以像 Sandybridge 一样消除异或归零但不消除移动。但我对此表示怀疑,因为它没有什么好处。)

正如 Cody 指出的那样,Agner Fog 的 table 表明 KNL 运行 在 FP0/1 上的 vxorps/dvpxord/q 具有相同的吞吐量和延迟,假设他们确实需要一个端口。我认为这仅适用于 xmm/ymm vxorps/d,除非英特尔的文档有误并且 EVEX vxorps zmm 可以 运行 在 KNL 上。

此外,在 Skylake 和更高版本上,非归零 vpxorvxorps 运行 在相同的端口上。矢量整数布尔值的 运行-on-more-ports 优势仅在 Intel Nehalem 到 Broadwell 上存在,即不支持 AVX512 的 CPUs。 (它甚至对 Nehalem 归零很重要,它实际上需要一个 ALU 端口,即使它被认为独立于旧值)。

Skylake 上的旁路延迟延迟取决于它碰巧选择的端口,而不是你使用的指令。即,如果 vandps 被安排到 p0 或 p1 而不是 p5,那么 vaddps 读取 vandps 的结果会有一个额外的延迟周期。请参阅 Intel 的优化手册以获得 table。更糟糕的是,这种额外的延迟永远适用,即使结果在读取之前在寄存器中存放了数百个周期。它影响从另一个输入到输出的 dep 链,所以在这种情况下它仍然很重要。 (TODO:写下我的实验结果,然后 post 把它们写在某处。)