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

12

假设我想清空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来测试 - 也许有人已经在骑士着陆上进行了测试?有没有发布任何延迟?

2
指令集,例如AVX2和AVX512,并不像你所暗示的那样决定性能。它取决于实际的微架构实现。Cannondale可能会有一个与Knights-Landing非常不同的AVX512实现。 - Ross Ridge
@RossRidge - 是的,你说得对。我已经更新了问题,我对骑士着陆感兴趣。 - Maxim Masiutin
2
据我理解AVX指令集,vpxor xmm, xmm, xmm会清除目标寄存器的上半部分。参考:Intel® 64和IA-32体系结构软件开发人员手册*2.3.10.1向量长度转换和编程注意事项[...]程序员应该记住,使用VEX.128和VEX.256前缀编码的指令将清除任何未来对向量寄存器的扩展[...] - EOF
1
编写一个使用内置函数的小测试程序,并查看像ICC这样的优秀编译器生成了什么。 - Paul R
@PaulR - 谢谢!好主意! - Maxim Masiutin
3个回答

13

最有效的方法是利用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位中使用源寄存器,即使目的地是xmm8-xmm15,也可以使用2字节VEX。(当第二个源寄存器为x / ymm8-15时需要3字节VEX前缀)。是的,只要两个源操作数是相同的寄存器(我测试了Skylake上它不使用执行单元),它仍然被认为是一个清零惯用语。
除了代码大小影响,性能在Skylake-AVX512和KNL上与vpxord/q zmm和vxorps zmm完全相同。 (较小的代码几乎总是更好的。)但请注意,KNL具有非常弱的前端,其中最大解码吞吐量仅能勉强饱和矢量执行单元,并且通常是Agner Fog's microarch guide所述的瓶颈。 (它没有uop缓存或循环缓冲区,并且最大吞吐量为每个时钟周期2条指令。此外,平均提取吞吐量限制为每个周期16B。)

此外,在假设的未来的AMD(或者可能是英特尔)CPU中,将AVX512指令解码为两个256b uops(或四个128b uops)会更加高效。当前的AMD CPU(包括Ryzen)直到解码vpxor ymm0, ymm0, ymm0为2个uops后才检测清零习语,所以这是一个真实的问题。旧版本的编译器出现了错误(gcc bug 80636, clang bug 32862),但这些优化错误已在当前版本中得到修复(GCC8、clang6.0、MSVC从一开始就修复了(?). ICC仍然不够优秀。)


将zmm16-31清零确实需要使用EVEX编码的指令vpxordvpxorq都是同样好的选择。EVEX vxorps由于某些原因需要AVX512DQ(在KNL上不可用),但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指令解码成多个uop。 如果您的目标具有AVX512DQ(Skylake-AVX512但不支持KNL),在创建FP数学指令的输入时使用vxorps可能是一个好主意,或者在任何其他情况下使用vpxord。对Skylake没有影响,但某些未来的CPU可能会关注这一点。如果总是使用vpxord更容易,请不要担心这个问题。
相关:在zmm寄存器中生成全1的最佳方法似乎是vpternlogd zmm0,zmm0,zmm0, 0xff。(使用全1的查找表,逻辑表中的每个条目都为1)。vpcmpeqd same,same无法工作,因为AVX512版本会将比较结果存储到一个掩码寄存器而不是向量寄存器中。
这种特殊情况的vpternlogd/q在KNL或Skylake-AVX512上没有独立的特殊情况,因此尽量选择一个未使用的寄存器。在SKL-avx512上相当快,根据我的测试,每个时钟周期可以处理2次。(如果需要多个全1的寄存器,请使用vpternlogd并复制结果,特别是如果您的代码只在Skylake上运行而不仅仅是在KNL上运行)。
我选择了32位元素大小(使用vpxord而不是vpxorq),因为32位元素大小被广泛使用,如果一个元素大小较慢,通常不是32位元素。例如,在Silvermont上,pcmpeqq xmm0,xmm0pcmpeqd xmm0,xmm0慢得多。在AVX512之前,pcmpeqw是生成全1向量的另一种方法,但gcc选择pcmpeqd。我非常确定对于xor-zeroing来说这永远不会有影响,特别是没有掩码寄存器,但如果你正在寻找选择vpxordvpxorq的理由,除非有人在任何AVX512硬件上发现真正的性能差异,否则这是一个很好的理由。

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


XOR-zeroing doesn't use an execution port at all on Intel SnB-family CPUs, including Skylake-AVX512. (TODO: incorporate some of this into that answer, and make some other updates to it...)

但是在KNL上,我相信xor-zeroing需要一个执行端口。两个向量执行单元通常可以跟上前端,因此在发出/重命名阶段处理xor-zeroing在大多数情况下不会产生性能差异。vmovdqa64/vmovaps根据Agner Fog的测试需要一个端口(更重要的是具有非零延迟),因此我们知道它不会在发出/重命名阶段处理这些操作。(它可能像Sandybridge一样消除xor-zeroing但不消除mov操作。但我怀疑这样做没有什么好处。)

如 Cody 指出,Agner Fog 的表格表明 KNL 在 FP0/1 上同时运行 vxorps/dvpxord/q,前提是它们需要一个端口,并且具有相同的吞吐量和延迟。我认为这仅适用于 xmm/ymm vxorps/d,除非英特尔的文档存在错误并且 EVEX vxorps zmm 可以在 KNL 上运行。
另外,在 Skylake 及更高版本中,非零化的 vpxorvxorps 运行在相同的端口上。矢量整数布尔运算在 Intel Nehalem 至 Broadwell,即不支持 AVX512 的 CPU 上才会占用更多的端口。(即使在 Nehalem 上进行零化,它也实际上需要一个 ALU 端口,尽管它被认为与旧值无关)。
Skylake上的旁路延迟取决于它选择哪个端口,而不是你使用了哪个指令。例如,如果将vandps调度到p0或p1,那么vaddps读取vandps的结果就会增加一个周期的延迟。请参阅英特尔的优化手册以获取表格。更糟糕的是,这种额外的延迟会永久存在,即使结果在寄存器中等待数百个周期才被读取。它会影响来自另一个输入到输出的依赖链,因此在这种情况下仍然很重要。(TODO:整理一下我的实验结果并发布到某个地方。)

我相信操作系统可以从ring 0更改它,但我不知道为什么会动态发生。通常,这将是类似于禁用AVX支持的引导标志之类的东西。而且,如果需要的话,我认为操作系统将负责发出VZEROUPPER,例如可能支持动态切换ISA支持的VM环境?我不知道这些是否存在! 我不确定的事情是在SSE兼容模式下运行时VLMAX是否设置为128([此处C状态](https://software.intel.com/en-us/articles/intel-avx-state-transitions-migrating-sse-code-to-avx))。 - Cody Gray
@CodyGray:啊,我明白了。请注意,SSE兼容模式不是一种在架构上可见的东西。它只表现为性能影响,因此您可以确信当前微架构的SSE/AVX“状态”不会改变指令对架构状态的影响。 - Peter Cordes
回复:缺少VZEROUPPER:如果它不是隐式发生的,那么这可能意味着没有它,CPU需要保留这些内容(例如,对于每个指令都有一个错误依赖关系)。并不是说它在“正常”的操作系统和虚拟机中有用或可用,但是CPU必须这样做,除非他们编写规则允许它不这样做。(例如,允许内容为“未定义”或必须为零,而不是在减少VLMAX之前的内容)。 - Peter Cordes
当然,这个问题特指骑士着陆处理器(Knights Landing),因为在那里使用VZEROUPPER速度非常慢。而且您甚至没有必要使用它。如果我没记错的话,混合遗留的SSE和VEX编码的AVX指令的性能惩罚是最小的。老实说,我对KNL上的运作还有点困惑。这是一个非常不同的世界,我没有其中一个可以玩。无论如何,这是减少指令大小的聪明解决方案。当我写答案时,我并没有考虑代码大小,所以我完全忽略了这一点。 - Cody Gray
@Cody:但这些只是性能方面的考虑。我想知道规则对正确性的影响。如果文档说在缩小VLMAX后必须保留上位位,然后使用AVX指令,然后增加VLMAX,那么硬件必须以某种方式实现这一点(否则就是文档或CPU错误)。我猜规则中的某些内容让它可以不保留该架构状态,否则可能需要特殊模式来给指令假依赖项,而他们似乎会在这方面花费晶体管,而不是编写规则来避免这种情况,这看起来很疯狂。 - Peter Cordes
显示剩余2条评论

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还尝试在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,在骑士着陆中:
  • 两者在相同数量的端口上执行(FP0或FP1),
  • 都解码为1个微操作
  • 都有至少2个最小延迟,并且往返吞吐量为0.5。
    (请注意,这个最后一个符号突出了KNL的一个重大缺点——所有矢量指令的延迟至少为2个时钟周期,即使是其他微构架上具有1个时钟周期延迟的简单指令也是如此。)
没有明显的优胜者,但编译器似乎更喜欢VPXORD,所以我也会坚持使用它。
关于VPXORD/VPXORQVXORPS/VXORPD,你在问题中提到,打包整数指令通常可以在更多的端口上执行,至少在英特尔CPU上是这样,这使得前者更可取。然而,在骑士着陆芯片上并非如此。无论是打包整数还是浮点数,所有逻辑指令都可以在FP0或FP1上执行,并且具有相同的延迟和吞吐量,因此理论上你可以使用任何一种形式。另外,由于这两种形式的指令都在浮点单元上执行,所以没有域交叉惩罚(转发延迟),即混合它们就像你会在其他微体系结构上看到的那样。我的结论?坚持使用整数形式。在KNL上不会变差,在优化其他体系结构时则会获胜,因此要保持一致。这样你需要记住的东西就更少了。优化已经足够困难了。
顺便提一下,当决定使用 VMOVAPS 还是 VMOVDQA64 时,同样的情况也适用。它们都是6字节指令,具有相同的延迟和吞吐量,在相同的端口上执行,并且没有旁路延迟需要考虑。在针对 Knights Landing 时,从实际目的来看,这些可以被视为等效的。
最后,你问到“当使用 VPXORD/VPXORQ 清除 ZMM 寄存器的先前值时,CPU 是否足够聪明以避免产生错误依赖”。我不确定,但我认为是的。自从很久以前就已经建立了使用异或寄存器本身清除的惯用语,其他英特尔 CPU 已知能够识别它,所以我无法想象为什么 KNL 不会识别它。但即使不是这样,这仍然是清除寄存器的最优方式。
另一种选择可能是从内存中移入一个0值,但这不仅是一条编码时间更长的指令,还需要支付内存访问惩罚。这不会带来任何好处,除非您的吞吐量受限,因为具有内存操作数的VMOVAPS在执行时使用了不同的单元(专用内存单元,而不是浮点单元之一)。然而,您需要一个非常有说服力的基准来证明这种优化决策的合理性。当然,这并不是一种“通用”策略。

或者您可以将寄存器与自身相减?但我怀疑这不太可能被认为是无依赖性,而且关于执行特性的其他所有内容都将保持不变,因此这不是从标准习惯中脱离的充分理由。

在这两种情况下,实用因素发挥了作用。当紧要关头时,您必须编写其他人可以阅读和维护的代码。由于这将导致每个以后阅读您代码的人都会遇到问题,因此您最好有一个真正令人信服的理由来做一些奇怪的事情。


下一个问题是:我们应该重复发出VPXORD指令,还是应该将一个清零的寄存器复制到其他寄存器中?
嗯,VPXORDVMOVAPS具有等效的延迟和吞吐量,解码为相同数量的µops,并且可以在相同数量的端口上执行。从这个角度来看,没有关系。
那数据依赖呢?天真地说,重复异或更好,因为移动依赖于初始异或。也许这就是为什么Clang更喜欢重复异或,而GCC则更喜欢在XOR和MOV之间安排其他指令的原因。如果我快速编写代码而不进行任何研究,我可能会像Clang一样编写它。但是我不能确定这是最优的方法,除非进行基准测试。由于我们都无法访问Knights Landing处理器,这些测试并不容易获得。 :-)
Intel的软件开发仿真器支持AVX-512,但不清楚它是否是适合基准测试/优化决策的周期精确模拟器。这份文档同时建议它既是(“Intel SDE对性能分析、编译器开发调优和库应用程序开发非常有用。”),又不是(“请注意,Intel SDE是一种软件仿真器,主要用于仿真未来的指令。它不是周期精确的,可能非常缓慢(高达100倍)。它不是性能精确的仿真器。”)。我们需要的是支持骑士着陆的IACA版本,但遗憾的是,这还没有出现。
总之,很高兴看到三个最受欢迎的编译器即使针对这样一种新架构也能生成高质量、高效的代码。它们在选择使用哪些指令方面略有不同,但这几乎没有实际意义。
在许多方面,我们已经看到这是由于 Knights Landing 微架构的独特方面。特别是,大多数向量指令在两个浮点单元中执行,并且它们具有相同的延迟和吞吐量,这意味着您无需担心域交叉惩罚,并且在优先选择打包整数指令而不是浮点指令时没有特殊的好处。您可以在核心图表中看到这一点(左侧的橙色块是两个向量单元):

Diagram/schematic of Intel's Knights Landing microprocessor core, showing there are only 2 vector units.

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


3
有一种特殊情况,使用“异或”而不是“移动”可以带来好处。当一个被清零的寄存器立即被另一个指令覆盖时,使用“移动”需要额外的被清零的寄存器进行传递,而使用“异或”则不需要。因此可能会导致寄存器压力增加。 - Mysticial
2
这种情况非常罕见。自从 AVX 以来,几乎所有的 SIMD 指令都是非破坏性的。唯一的例外是 FMAs、2-reg permutes 和 blend-masking。对于零输入,FMAs 会退化,blend-masking 会降为零掩码。因此,剩下的只有 2-reg permutes 和 IFMA52。即使在这些情况下,你也必须用完 32 个寄存器才会有影响。 - Mysticial
2
@MaximMasiutin:混合使用VEX和EVEX是完全没有问题的,因为AVX被正确设计以避免SSE/AVX混合问题(通过隐式地将VLMAX清零,正如您所指出的)。这就是为什么vpxor xmm15,xmm0,xmm0是清零zmm15的最佳方式(4字节指令而不是6字节,正如我在我的回答中解释的那样)。 - Peter Cordes
1
此外,@Cody:令人惊讶的是,EVEX vxorps zmm 需要 AVX512DQ,而 KNL 不支持!因此,vpxord/q 是您唯一的好选择。 - Peter Cordes
1
我非常确定,重复使用xorxor+mov更好,除非是在像AMD Bulldozer系列这样的CPU上,其中xor零化需要执行端口但向量移动已被消除。差异很小,以至于gcc没有为-mtune=intel或任何其他东西改变这一点。 - Peter Cordes
显示剩余16条评论

2
我编写了一个使用内置函数和ICC 17编译的简单C测试程序 - 在-O3优化级别下,我得到的用于将4个zmm寄存器清零的生成代码如下:
    vpxord    %zmm3, %zmm3, %zmm3                           #7.21
    vmovaps   %zmm3, %zmm2                                  #8.21
    vmovaps   %zmm3, %zmm1                                  #9.21
    vmovaps   %zmm3, %zmm0                                  #10.21

谢谢!数字#7.21是什么意思? - Maxim Masiutin
1
它们只是编译器添加的注释,@Maxim。7、8、9和10是源代码中的行号。21似乎是内部函数开始的列号。 - Cody Gray
为什么编译器不直接使用EVEX前缀指令来清除256位寄存器 - 它应该自动清除最高的511-256位,不是吗? - Maxim Masiutin
@MaximMasiutin:你是不是写反了?使用 vpxor ymm0,ymm0,ymm0 来清除 zmm0?如果你只关心 ymm 部分,我不知道为什么你要使用 EVEX 指令。反过来倒是个好主意,请看我的回答 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接