10

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

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

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

在 AVX2 上,如果我想清除ymm寄存器,vpxor它是最快的,比 更快vxorps,因为vpxor可以在多个单元上运行。

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

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

4

3 回答 3

11

最有效的方法是利用 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 上的执行单元)。

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

此外,在假设的未来 AMD(或者可能是 Intel)CPU 上,将 AVX512 指令解码为两个 256b 微指令(或四个 128b 微指令),效率更高。 当前的 AMD CPU(包括 Ryzen)直到解码到 2 微秒后才检测到归零习语vpxor ymm0, ymm0, ymm0,所以这是真实的。旧的编译器版本出错了(gcc bug 80636clang bug 32862),但是那些错过优化的错误在当前版本中得到了修复(GCC8,clang6.0,MSVC since forever(?)。ICC仍然次优。)


清零 zmm16-31 确实需要 EVEX 编码指令vpxord或者vpxorq同样是不错的选择。 EVEXvxorps出于某种原因需要 AVX512DQ(在 KNL 上不可用),但EVEXvpxord/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, ...在未来将 512b 指令解码为多个微指令的 CPU 上使用以获得更好的性能。

如果您的目标具有 AVX512DQ(Skylake-AVX512 但不是 KNL),则在为 FP 数学指令或任何其他情况vxorps创建输入时使用它可能是一个好主意。vpxord对 Skylake 没有影响,但未来的一些 CPU 可能会关心。如果总是更容易使用,请不要担心这一点vpxord


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

这种特殊情况vpternlogd/q在 KNL 或 Skylake-AVX512 上并不独立,因此请尝试选择冷寄存器。不过,根据我的测试,在 SKL-avx512 上它非常快:每时钟吞吐量 2 次。(如果您需要多个全 1 的 reg,请在 vpternlogd 上使用并复制结果,尤其是如果您的代码将在 Skylake 而不仅仅是 KNL 上运行)。


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

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


XOR 归零在 Intel SnB 系列 CPU 上根本不使用执行端口,包括 Skylake-AVX512。(TODO:将其中的一些合并到那个答案中,并对其进行一些其他更新......)

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

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

此外,在 Skylake 及更高版本上,非归零vpxorvxorps在相同端口上运行。向量整数布尔值在更多端口上运行的优势仅在 Intel Nehalem 到 Broadwell 上存在,即不支持 AVX512 的 CPU。(即使在 Nehalem 上归零也很重要,它实际上需要一个 ALU 端口,即使它被认为与旧值无关)。

Skylake 上的旁路延迟延迟取决于它碰巧选择的端口,而不是您使用的指令。即,如果将 a安排到 p0 或 p1 而不是 p5,则vaddps读取 a 的结果vandps会有一个额外的延迟周期。vandps请参阅英特尔的优化手册以获取表格。更糟糕的是,这种额外的延迟会永远存在,即使结果在被读取之前在寄存器中存在数百个周期。它会影响从另一个输入到输出的 dep 链,因此在这种情况下它仍然很重要。(TODO:写下我对此的实验结果并将它们张贴在某个地方。)

于 2017-06-30T07:34:04.000 回答
5

遵循 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 还尝试在 和 之间安排其他VPXORD指令VMOVDQA64。ICC 没有表现出这种偏好。

ClangVPXORD独立使用将所有 ZMM 寄存器归零,例如

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 的指令表,在 Knights Landing:

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

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

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

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

最后,您问“CPU [是] 是否足够聪明,不会在 [您] 用VPXORD/清除 ZMM 寄存器的先前值时对它们产生错误的依赖关系VPXORQ”。嗯,我不确定,但我想是的。很长一段时间以来,将寄存器与自身进行异或清除它已经成为一种既定的习惯用法,并且众所周知它可以被其他 Intel CPU 识别,所以我无法想象为什么它不会出现在 KNL 上。但即使不是,这仍然是清除寄存器的最佳方式。

另一种方法是从内存中移入一个 0 值,这不仅是一条更长的编码指令,而且还需要您支付内存访问费用。这不会是一场胜利……除非您可能受到吞吐量限制,因为VMOVAPS内存操作数在不同的单元(专用内存单元,而不是任何一个浮点单元)上执行。不过,您需要一个非常引人注目的基准来证明这种优化决策的合理性。这当然不是“通用”策略。

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

在这两种情况下,实用性因素都会发挥作用。紧要关头,您必须编写代码供其他人阅读和维护。因为它会导致每个阅读你的代码的人永远绊倒,所以你最好有一个真正令人信服的理由来做一些奇怪的事情。


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

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

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

英特尔的软件开发仿真器确实支持 AVX-512,但目前尚不清楚这是否是一个适合基准测试/优化决策的周期精确仿真器。本文档同时表明它是(“英特尔 SDE 可用于性能分析、编译器开发调优和库的应用程序开发。”)和不是(“请注意,英特尔 SDE 是一个软件仿真器,主要用于用于模拟未来的指令。它不是周期精确的,并且可能非常慢(高达 100 倍)。它不是性能精确的模拟器。”)。我们需要的是支持 Knights Landing 的IACA版本,但遗憾的是,它还没有出现。


总之,很高兴看到三个最流行的编译器即使对于这样一个新架构也能生成高质量、高效的代码。他们会做出稍微不同的决定来选择更喜欢的指令,但这几乎没有实际差异。

在许多方面,我们已经看到这是因为 Knights Landing 微架构的独特方面。特别是,大多数向量指令在两个浮点单元中的任何一个上执行,并且它们具有相同的延迟和吞吐量这一事实,这意味着您不需要关注跨域惩罚,并且您没有优先使用压缩整数指令而不是浮点指令的特别好处。您可以在核心图中看到这一点(左侧的橙色块是两个向量单元):

英特尔 Knights Landing 微处理器内核的示意图/示意图,显示只有 2 个矢量单元。

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

于 2017-06-16T09:17:20.453 回答
2

使用内部函数编写了一个简单的 C 测试程序,并使用 ICC 17 进行了编译——我得到的用于将 4 个 zmm 寄存器(at -O3)归零的生成代码是:

    vpxord    %zmm3, %zmm3, %zmm3                           #7.21
    vmovaps   %zmm3, %zmm2                                  #8.21
    vmovaps   %zmm3, %zmm1                                  #9.21
    vmovaps   %zmm3, %zmm0                                  #10.21
于 2017-06-16T06:48:40.533 回答