对于大多数具有固定宽度指令的体系结构,答案可能是一个无聊的单指令mov 符号扩展或反转立即数,或 mov lo/high 对。例如在 ARM 上,mvn r0, #0(不移动)。请参阅 x86、ARM、ARM64 和 MIPS 的 gcc asm 输出,on the Godbolt compiler explorer。 IDK 任何关于 zseries asm 或机器代码的信息。
在 ARM 中,eor r0,r0,r0 明显比 mov-immediate 差。它取决于旧值,没有特殊情况处理。内存依赖排序规则 prevent an ARM uarch from special-casing it even if they wanted to. 对于大多数其他具有弱排序内存的 RISC ISA 也是如此,但对于 memory_order_consume(在 C++11 术语中)不需要屏障。
x86 xor-zeroing 是特殊的,因为它的可变长度指令集。
从历史上看,8086 xor ax,ax 直接很快因为它很小。由于该惯用语被广泛使用(并且归零比全部使用更常见),CPU 设计人员给予了特别支持,现在xor eax,eax 在 Intel Sandybridge 系列和其他一些 CPU 上比 mov eax,0 更快,即使没有考虑直接和间接的代码大小效应。请参阅What is the best way to set a register to zero in x86 assembly: xor, mov or and?,了解我能够挖掘到的尽可能多的微架构优势。
如果 x86 有一个固定宽度的指令集,我想知道mov reg, 0 是否会得到与异或归零一样多的特殊处理?也许是因为在编写 low8 或 low16 之前打破依赖关系很重要。
最佳性能的标准选项:
-
mov eax, -1:5 个字节,使用mov r32, imm32 编码。 (不幸的是,没有符号扩展mov r32, imm8)。在所有 CPU 上都有出色的性能。 6 个字节用于 r8-r15(REX 前缀)。
-
mov rax, -1:7 个字节,使用mov r/m64, sign-extended-imm32 编码。 (不是eax 版本的 REX.W=1 版本。那将是 10 字节 mov r64, imm64)。在所有 CPU 上都表现出色。
节省一些代码大小的奇怪选项通常以牺牲性能为代价:
-
xor eax,eax/dec rax(或not rax):5 个字节(4 个用于 32 位 eax)。缺点:前端有两个微指令。最近英特尔上的调度程序/执行单元仍然只有一个未融合域微指令,其中xor-zeroing 在前端处理。 mov-immediate 总是需要一个执行单元。 (但对于可以使用任何端口的指令而言,整数 ALU 吞吐量很少成为瓶颈;额外的前端压力是问题所在)
-
xor ecx,ecx / lea eax, [rcx-1] 2 个常量共 5 个字节(rax 为 6 个字节):留下一个单独的归零寄存器。如果您已经想要一个归零的寄存器,那么这几乎没有缺点。在大多数 CPU 上,lea 可以在比mov r,i 更少的端口上运行,但由于这是新依赖链的开始,CPU 可以在它发出后的任何空闲执行端口周期中运行它。
如果您使用mov reg, imm32 执行第一个常量,使用lea r32, [base + disp8] 执行第二个常量,则相同的技巧适用于任何两个附近的常量。 disp8 的范围是 -128 到 +127,否则您需要 disp32。
or eax, -1:3 个字节(rax 为 4 个),使用 or r/m32, sign-extended-imm8 编码。缺点:对寄存器旧值的错误依赖。
-
push -1 / pop rax:3 个字节。缓慢但很小。仅推荐用于漏洞利用/代码高尔夫。 适用于任何 sign-extended-imm8,与大多数其他人不同。
缺点:
- 使用存储和加载执行单元,而不是 ALU。 (在 AMD Bulldozer 系列中只有两个整数执行管道的极少数情况下,可能具有吞吐量优势,但解码/发出/退出吞吐量高于此。但未经测试请勿尝试。)
- 存储/重新加载延迟意味着
rax 在 Skylake 上执行后大约 5 个周期内不会准备好。
- (Intel):将堆栈引擎置于 rsp-modified 模式,因此下次您直接读取
rsp 时,它将采用堆栈同步 uop。 (例如add rsp, 28,或mov eax, [rsp+8])。
- 存储可能会在缓存中丢失,从而触发额外的内存流量。 (如果您没有在一个长循环中触及堆栈,则可能)。
向量 reg 不同
使用 pcmpeqd xmm0,xmm0 将向量寄存器设置为全是在大多数 CPU 上作为依赖破坏(不是 Silvermont/KNL)的特殊情况,但仍需要一个执行单元来实际写入这些寄存器. pcmpeqb/w/d/q 一切正常,但 q 在某些 CPU 上速度较慢。
对于 AVX2,ymm 等效的 vpcmpeqd ymm0, ymm0, ymm0 也是最佳选择。
对于 没有 AVX2 的 AVX,选择不太明确:没有一种明显的最佳方法。编译器使用various strategies:gcc 更喜欢用vmovdqa 加载一个32 字节的常量,而旧的clang 使用128 位vpcmpeqd,后跟一个交叉通道vinsertf128 来填充高半部分。较新的 clang 使用 vxorps 将寄存器归零,然后使用 vcmptrueps 将其填充为 1。这是vpcmpeqd 方法的道德等价物,但需要vxorps 来打破对寄存器先前版本的依赖,并且vcmptrueps 的延迟为3。它是一个合理的默认选择。
从 32 位值执行 vbroadcastss 可能比加载方法更好,但很难让编译器生成它。
最好的方法可能取决于周围的代码。
Fastest way to set __m256 value to all ONE bits
AVX512 比较只能使用掩码寄存器(如 k0)作为目标,因此编译器当前使用 vpternlogd zmm0,zmm0,zmm0, 0xff 作为 512b 全一成语。 (0xff 使 3 输入真值表的每个元素都成为 1)。这并不是对 KNL 或 SKL 的依赖破坏的特殊情况,但它在 Skylake-AVX512 上具有每时钟 2 个吞吐量。这优于使用更窄的依赖关系破坏 AVX all-ones 并广播或改组它。
如果您需要在循环中重新生成全一,显然最有效的方法是使用vmov* 复制全一寄存器。这甚至不使用现代 CPU 上的执行单元(但仍占用前端问题带宽)。但如果向量寄存器用完了,加载常量或[v]pcmpeq[b/w/d] 是不错的选择。
对于 AVX512,值得尝试 VPMOVM2D zmm0, k0 或 VPBROADCASTD zmm0, eax。每个都有 only 1c throughput,但它们应该打破对 zmm0 旧值的依赖(与 vpternlogd 不同)。它们需要一个掩码或整数寄存器,您在循环外使用 kxnorw k1,k0,k0 或 mov eax, -1 进行初始化。
对于 AVX512 掩码寄存器,kxnorw k1,k0,k0 有效,但它不会破坏当前 CPU 的依赖关系。 Intel's optimization manual 建议在收集指令之前使用它来生成全一,但建议避免使用与输出相同的输入寄存器。这避免了使一个独立的集合依赖于循环中的前一个集合。由于k0 经常未被使用,因此通常是一个不错的读取选择。
我认为vpcmpeqd k1, zmm0,zmm0 会起作用,但它可能不是作为不依赖于 zmm0 的 k0=1 习惯用法的特殊情况。 (要设置所有 64 位而不是低 16 位,请使用 AVX512BW vpcmpeqb)
在 Skylake-AVX512 上,k 指令在屏蔽寄存器 only run on a single port 上运行,甚至是像 kandw 这样的简单指令。 (另请注意,当管道中有任何 512b 操作时,Skylake-AVX512 不会在端口 1 上运行向量微指令,因此执行单元吞吐量可能是一个真正的瓶颈。)
没有kmov k0, imm,只能从整数或内存中移动。可能没有k 指令相同,相同被检测为特殊,因此问题/重命名阶段的硬件不会为k 寄存器寻找它。