以下所有指令均执行相同的操作:将 %eax
设置为零。哪种方式最优(需要最少的机器周期)?
xorl %eax, %eax
mov $0, %eax
andl $0, %eax
以下所有指令均执行相同的操作:将 %eax
设置为零。哪种方式最优(需要最少的机器周期)?
xorl %eax, %eax
mov $0, %eax
andl $0, %eax
xor same, same
是所有 CPU 的最佳选择。没有其他方法比它更有优势,而且它至少比任何其他方法都有一些优势。它是 Intel 和 AMD 官方推荐的,并且编译器也采用这种方法。在64位模式下,仍然使用xor r32, r32
,因为写入一个32位寄存器会将高32位清零。xor r64, r64
浪费了一个字节,因为它需要一个 REX 前缀。
更糟糕的是,Silvermont 只将xor r32,r32
识别为打断依赖关系,而不是64位操作数大小。因此,即使仍然需要一个 REX 前缀,因为你要清零 r8..r15,也要使用xor r10d,r10d
,而不是xor r10,r10
。
GP-整数示例:
xor eax, eax ; RAX = 0. Including AL=0 etc.
xor r10d, r10d ; R10 = 0. Still prefer 32-bit operand-size.
xor edx, edx ; RDX = 0
; small code-size alternative: cdq ; zero RDX if EAX is already zero
; SUB-OPTIMAL
xor rax,rax ; waste of a REX prefix, and extra slow on Silvermont
xor r10,r10 ; bad on Silvermont (not dep breaking), same as r10d on other CPUs because a REX prefix is still needed for r10d or r10.
mov eax, 0 ; doesn't touch FLAGS, but not faster and takes more bytes
and eax, 0 ; false dependency. (Microbenchmark experiments might want this)
sub eax, eax ; same as xor on most but not all CPUs; bad on Silvermont for example.
xor cl, cl ; false dep on some CPUs, not a zeroing idiom. Use xor ecx,ecx
mov cl, 0 ; only 2 bytes, and probably better than xor cl,cl *if* you need to leave the rest of ECX/RCX unmodified
pxor xmm, xmm
来将向量寄存器清零。这通常是gcc的做法(即使在使用FP指令之前)。
xorps xmm, xmm
也是可以的。它比pxor
短一个字节,但xorps
需要在Intel Nehalem上执行端口5,而pxor
可以在任何端口(0/1/5)上运行。(Nehalem的整数和FP之间的2c旁路延迟通常不相关,因为乱序执行通常可以在新的依赖链开始时隐藏它)。xorps
和pxor
以相同的方式处理(作为向量整数指令)。vpxor xmm, xmm, xmm
是一个很好的选择。虽然vpxor ymm, ymm, ymm
在编码时不需要额外的字节,而且在Intel上运行相同,但在AMD Zen2之前的处理器上速度较慢(2个微操作)。AVX512 ZMM清零需要额外的字节(用于EVEX前缀),因此应优先选择XMM或YMM清零。
XMM/YMM/ZMM示例
# Good:
xorps xmm0, xmm0 ; smallest code size (for non-AVX)
pxor xmm0, xmm0 ; costs an extra byte, runs on any port on Nehalem.
xorps xmm15, xmm15 ; Needs a REX prefix but that's unavoidable if you need to use high registers without AVX. Code-size is the only penalty.
# Good with AVX:
vpxor xmm0, xmm0, xmm0 ; zeros X/Y/ZMM0
vpxor xmm15, xmm0, xmm0 ; zeros X/Y/ZMM15, still only 2-byte VEX prefix
#sub-optimal AVX
vpxor xmm15, xmm15, xmm15 ; 3-byte VEX prefix because of high source reg
vpxor ymm0, ymm0, ymm0 ; decodes to 2 uops on AMD before Zen2
# Good with AVX512
vpxor xmm15, xmm0, xmm0 ; zero ZMM15 using an AVX1-encoded instruction (2-byte VEX prefix).
vpxord xmm30, xmm30, xmm30 ; EVEX is unavoidable when zeroing zmm16..31, but still prefer XMM or YMM for fewer uops on probable future AMD. May be worth using only high regs to avoid needing vzeroupper in short functions.
# Good with AVX512 *without* AVX512VL (e.g. KNL / Xeon Phi)
vpxord zmm30, zmm30, zmm30 ; Without AVX512VL you have to use a 512-bit instruction.
# sub-optimal with AVX512 (even without AVX512VL)
vpxord zmm0, zmm0, zmm0 ; EVEX prefix (4 bytes), and a 512-bit uop. Use AVX1 vpxor xmm0, xmm0, xmm0 even on KNL to save code size.
请参阅在AMD Jaguar/Bulldozer/Zen上,使用xmm寄存器是否比ymm寄存器更快执行vxorps-zeroing操作?和
在Knights Landing上清除单个或多个ZMM寄存器的最有效方法是什么?
k0..7
掩码寄存器。SSE/AVX vpcmpeqd
在许多情况下都能打破依赖(尽管仍需要一个微操作来写入1),但AVX512 vpternlogd
对于ZMM寄存器甚至不能打破依赖。在循环内部,考虑从另一个寄存器复制而不是使用ALU微操作重新创建具有AVX512的寄存器。mov reg,0
的代码大小更小。 (所有CPU)xor
时不需要执行单元(在未融合域中为零个uop),但忽略了它在融合域中仍然是一个uop。现代英特尔CPU每个时钟周期可以发射和退役4个融合域uop。这就是每个时钟周期限制为4个的原因。增加寄存器重命名硬件的复杂性只是限制设计宽度的原因之一。(Bruce写了一些非常优秀的博客文章,比如他关于FP数学和x87/SSE/舍入问题的系列文章,我强烈推荐阅读)。
; Example 7.9. Partial register problem avoided in loop
xor eax, eax
mov ecx, 100
LL:
mov al, [esi]
mov [edi], eax ; No extra uop
inc esi
add edi, 4
dec ecx
jnz LL
mov reg, 0
并不被认为是一个清零惯用语。如果它们在后期CPU上检测到了,我会感到非常惊讶。
xor
设置标志位,这意味着在测试条件时必须小心。由于setcc
只能用于8位目标寄存器(直到APX扩展1),通常需要注意避免部分寄存器惩罚。
如果x86-64重新使用了其中一个已删除的操作码(比如AAM),用于16/32/64位setcc r/m
,并将谓词编码放在r/m字段的源寄存器的3位字段中(方式类似于其他一元操作指令将其作为操作码位使用)。但他们没有这样做,并且对于x86-32也没有帮助。
理想情况下,你应该使用xor
/ 设置标志 / setcc
/ 读取完整的寄存器:
...
call some_func
xor ecx,ecx ; zero *before* setting FLAGS
cmp eax, 42
setnz cl ; ecx = cl = (some_func() != 42)
add ebx, ecx ; no partial-register penalty here
ebx += (eax != 0)
,则可以使用cmp eax, 1; sbb ebx, -1
的技巧,使用进位标志与adc
或sbb
直接添加或减去它,而不是将其实现为0/1整数,正如@l4m2在评论中指出的那样。 如果很难在设置FLAGS之前进行xor-zero,则甚至可能值得做sub eax, 42
(或LEA到另一个寄存器)/ cmp eax,1
/ sbb
。 特别是如果很难安排在设置FLAGS之前进行xor-zero,因为cmp
/setcc
/movzx
/add
对于延迟来说都是关键路径上的4个操作。setcc
/ movzx r32, r8
可能是Intel P6的最佳替代方案,如果在设置标志指令之前无法进行xor-zero操作。这应该比在xor-zero操作后重复测试要好。(甚至不要考虑 / 或 / )。IvB和之后的处理器(除了Ice Lake)可以消除movzx r32, r8
(即通过寄存器重命名来处理,没有执行单元或延迟,就像xor-zero操作一样)。AMD Zen系列只能消除常规的mov
指令,所以movzx
需要一个执行单元并具有非零延迟,使得test/setcc
/movzx
比xor
/test/setcc
更差。
也比test/mov r,0
/setcc
更差(但在旧的Intel CPU上具有部分寄存器停顿时要好得多)。and
与立即零在我所知的任何CPU上都没有被特殊处理为独立于旧值,因此它不会破坏依赖链。它没有比xor
更多的优势,但有很多劣势。
它只在编写微基准测试时有用,当您希望将依赖作为延迟测试的一部分,并且希望通过清零和添加来创建一个已知值时。
请参考http://agner.org/optimize/以获取微架构细节,包括哪些清零习语被识别为打破依赖关系(例如,sub same,same
在某些CPU上被识别,而不是所有CPU,而xor same,same
在所有CPU上都被识别)。mov
确实打破了对寄存器旧值的依赖链(无论源值是零还是非零,因为这就是mov
的工作方式)。xor
只有在特殊情况下,即源和目标是同一个寄存器时才会打破依赖链,这就是为什么mov
没有列入特别识别的打破依赖关系的列表中。(此外,因为它没有被识别为清零习语,也就没有带来其他好处。)
setcc r64
。(指令总长度为6字节,在EVEX前缀中使用多余的位来编码寄存器目的地的传统与零扩展行为之间的差异。)
如果它确实能使你的代码更好或减少指令数,那么当然可以使用mov
将寄存器清零,以避免影响标志位,只要它不会引入除代码大小以外的性能问题。避免破坏标志位是不使用xor
的唯一合理理由,但有时候你可以在设置标志位之前进行异或清零,如果你有一个多余的寄存器。
setcc
之前使用mov
清零比在setcc
之后使用movzx reg32, reg8
(除非在英特尔上可以选择不同的寄存器)对延迟更好,但代码大小更大。
mov reg, src
也会打破OO CPU的依赖链。这种依赖关系的打破在优化手册中没有被提及,因为它不是一种特殊情况,只有当src和dest是同一个寄存器时才会发生。对于不依赖于其目标操作数的指令,它总是发生的(除了英特尔实现的popcnt/lzcnt/tzcnt
对目标操作数存在虚假依赖的情况)。 - Peter Cordesmov
变得免费,只会变成零延迟。通常,“不占用执行端口”的部分并不重要。融合域吞吐量很容易成为瓶颈,特别是在混合负载或存储器操作中。 - Peter Cordes