遵循 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
/VPXORQ
对VXORPS
/怎么样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 微架构的独特方面。特别是,大多数向量指令在两个浮点单元中的任何一个上执行,并且它们具有相同的延迟和吞吐量这一事实,这意味着您不需要关注跨域惩罚,并且您没有优先使用压缩整数指令而不是浮点指令的特别好处。您可以在核心图中看到这一点(左侧的橙色块是两个向量单元):

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