在x86汇编中,将寄存器置零的最佳方式是什么:xor、mov还是and?

186

以下所有指令均执行相同的操作:将 %eax 设置为零。哪种方式最优(需要最少的机器周期)?

xorl   %eax, %eax
mov    $0, %eax
andl   $0, %eax

10
请参考这篇文章,了解清零寄存器的一些微妙之处。 - Michael Petch
3
在x86汇编语言中,使用“xor reg, reg”指令将寄存器的值设置为零,而使用“mov reg, 0”指令也可以实现相同的效果。这两种方法都可以用来清空寄存器的值,但使用“xor”指令可以更快地完成操作,因为该指令不需要从内存中读取常数值。因此,在需要频繁清空寄存器值的情况下,使用“xor”指令可以提高代码的性能。 - Ciro Santilli OurBigBook.com
1个回答

321
TL;DR 总结: 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旁路延迟通常不相关,因为乱序执行通常可以在新的依赖链开始时隐藏它)。
在SnB系列微架构中,甚至没有一种xor-zeroing需要执行端口。在AMD和Nehalem之前的P6/Core2 Intel上,xorpspxor以相同的方式处理(作为向量整数指令)。
使用AVX版本的128位向量指令会将寄存器的上半部分清零,因此对于清零YMM(AVX1/AVX2)或ZMM(AVX512),或任何未来的向量扩展,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寄存器的最有效方法是什么?

半相关:将__m256值设置为所有位都是1的最快方法
高效地将CPU寄存器中的所有位设置为1也涵盖了AVX512 k0..7掩码寄存器。SSE/AVX vpcmpeqd在许多情况下都能打破依赖(尽管仍需要一个微操作来写入1),但AVX512 vpternlogd对于ZMM寄存器甚至不能打破依赖。在循环内部,考虑从另一个寄存器复制而不是使用ALU微操作重新创建具有AVX512的寄存器。
但清零是廉价的:在循环内部使用xor清零xmm寄存器通常与复制一样好,除了一些AMD CPU(Bulldozer和Zen)需要一个ALU微操作来写入零以外,因为它们对于向量寄存器具有mov消除功能。
零化成语(如xor)在各种微架构上有何特殊之处?
一些CPU将“sub same, same”识别为与“xor”类似的零化成语,但是所有能够识别任何零化成语的CPU都会识别“xor”。只需使用“xor”,这样就不必担心哪个CPU识别哪个零化成语。
与“mov reg, 0”(不被认为是零化成语)相比,“xor”具有一些明显和一些微妙的优势(先列出摘要清单,然后我会详细展开)。
  • mov reg,0的代码大小更小。 (所有CPU)
  • 避免后续代码的部分寄存器惩罚。 (Intel P6系列和SnB系列)
  • 不使用执行单元,节省功耗并释放执行资源。 (Intel SnB系列)
  • 较小的uop(无立即数据)在需要时为附近的指令提供了在uop缓存行中借用的空间。 (Intel SnB系列)
  • 不占用物理寄存器文件的条目。 (至少是Intel SnB系列(和P4),可能也适用于AMD,因为它们使用类似的PRF设计,而非像Intel P6系列微架构那样将寄存器状态保留在ROB中。)

较小的机器码大小(2个字节而不是5个)始终是一个优势:较高的代码密度导致更少的指令缓存未命中,以及更好的指令获取和潜在的解码带宽。
在Intel SnB系列微架构上,不使用执行单元进行异或运算的好处微乎其微,但可以节省功耗。这更有可能对SnB或IvB产生影响,因为它们只有3个ALU执行端口。Haswell及以后的处理器有4个执行端口,可以处理整数ALU指令,包括mov r32, imm32,因此在调度程序做出完美决策(实际情况并非总是如此)的情况下,即使所有指令都需要ALU执行端口,HSW仍然能够每个时钟周期维持4个uops。
有关更多详细信息,请参阅我在另一个问题中的回答Bruce Dawson的博客文章,Michael Petch在评论中链接的文章指出,在寄存器重命名阶段处理xor时不需要执行单元(在未融合域中为零个uop),但忽略了它在融合域中仍然是一个uop。现代英特尔CPU每个时钟周期可以发射和退役4个融合域uop。这就是每个时钟周期限制为4个的原因。增加寄存器重命名硬件的复杂性只是限制设计宽度的原因之一。(Bruce写了一些非常优秀的博客文章,比如他关于FP数学和x87/SSE/舍入问题的系列文章,我强烈推荐阅读)。
在AMD Bulldozer系列的CPU上,mov immediate指令与xor指令在相同的EX0/EX1整数执行端口上运行。mov reg,reg指令也可以在AGU0/1上运行,但这只适用于寄存器复制,而不是从立即数设置。所以据我所知,在AMD上,xor指令相对于mov指令的唯一优势就是编码更短。它可能还能节省物理寄存器资源,但我还没有看到任何测试结果。
识别清零习语,避免在Intel CPU上出现部分寄存器惩罚,这些CPU将部分寄存器与完整寄存器分开重命名(P6和SnB系列)。
使用“xor”将标记寄存器的高位部分清零,因此“xor eax, eax” / “inc al” / “inc eax”可以避免先IvB CPU通常存在的部分寄存器惩罚。即使没有使用“xor”,IvB及更高版本只有在修改高8位(AH)并且读取整个寄存器时才需要合并uop。(Agner错误地表示Haswell消除了AH合并惩罚。)
来自Agner Fog的微架构指南,第98页(Pentium M部分,被后续包括SnB在内的部分引用):
引用: 处理器识别将寄存器与自身进行异或运算时将其设置为零。寄存器中的特殊标记记住了寄存器的高位为零,以便EAX = AL。即使在循环中,该标记也会被记住。
    ; 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

(来自第82页)只要没有发生中断、预测错误或其他序列化事件,处理器将记住EAX的高24位为零。
该指南的第82页还确认了,在早期P6设计(如PIII或PM)上,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

这在所有CPU上都有最佳性能(没有停顿、合并uops或错误依赖关系)。 (如果条件是ebx += (eax != 0),则可以使用cmp eax, 1; sbb ebx, -1的技巧,使用进位标志与adcsbb直接添加或减去它,而不是将其实现为0/1整数,正如@l4m2在评论中指出的那样。 如果很难在设置FLAGS之前进行xor-zero,则甚至可能值得做sub eax, 42(或LEA到另一个寄存器)/ cmp eax,1 / sbb。 特别是如果很难安排在设置FLAGS之前进行xor-zero,因为cmp/setcc/movzx/add对于延迟来说都是关键路径上的4个操作。
当你不想在设置标志位的指令之前进行异或操作时,事情就变得更加复杂了。例如,你想根据一个条件进行分支,然后根据另一个条件设置cc(条件码)寄存器,这两个条件都来自同一个标志位。例如,cmp/jle,sete,如果你没有多余的寄存器,或者你想完全避免在未执行代码路径中使用xor操作。
没有被公认的不影响标志位的清零习语,所以最好的选择取决于目标微架构。在Core2上,插入合并的微操作可能会导致2到3个周期的停顿。在SnB上,代价较低,最坏情况下只有1个周期,而Haswell和更新的处理器不会单独重命名部分寄存器。在最近的CPU上,使用mov reg, 0 / setcc可能是最好的选择,但在旧的Intel CPU(Nehalem及更早版本)上会有显著的性能损失。在更新的CPU上,它几乎与xor清零一样好,但代码大小比movzx要大。
使用setcc / movzx r32, r8可能是Intel P6的最佳替代方案,如果在设置标志指令之前无法进行xor-zero操作。这应该比在xor-zero操作后重复测试要好。(甚至不要考虑 / 或 / )。IvB和之后的处理器(除了Ice Lake)可以消除movzx r32, r8(即通过寄存器重命名来处理,没有执行单元或延迟,就像xor-zero操作一样)。AMD Zen系列只能消除常规的mov指令,所以movzx需要一个执行单元并具有非零延迟,使得test/setcc/movzxxor/test/setcc更差。 也比test/mov r,0/setcc更差(但在旧的Intel CPU上具有部分寄存器停顿时要好得多)。
使用`setcc` / `movzx`在AMD/P4/Silvermont上没有先清零是不好的,因为它们不会单独跟踪子寄存器的依赖关系。这将导致对寄存器旧值的错误依赖。当`xor`/test/`setcc`不可行时,使用`mov reg, 0`/`setcc`进行清零/打破依赖可能是最好的选择。至少对于这是重要延迟链的“热”代码而言是如此。否则,可以选择`movzx`以节省一些代码大小。
当然,如果您不需要`setcc`的输出比8位更宽,您就不需要清零任何东西。但是,请注意,在选择最近作为长依赖链的一部分的寄存器时,要小心在P6/SnB之外的CPU上出现错误依赖。(并且要注意,如果调用可能保存/恢复正在使用的寄存器的函数,可能会导致部分寄存器停顿或额外的微操作。)

and与立即零在我所知的任何CPU上都没有被特殊处理为独立于旧值,因此它不会破坏依赖链。它没有比xor更多的优势,但有很多劣势。

它只在编写微基准测试时有用,当您希望将依赖作为延迟测试的一部分,并且希望通过清零和添加来创建一个已知值时。


请参考http://agner.org/optimize/以获取微架构细节,包括哪些清零习语被识别为打破依赖关系(例如,sub same,same在某些CPU上被识别,而不是所有CPU,而xor same,same在所有CPU上都被识别)。mov确实打破了对寄存器旧值的依赖链(无论源值是零还是非零,因为这就是mov的工作方式)。xor只有在特殊情况下,即源和目标是同一个寄存器时才会打破依赖链,这就是为什么mov没有列入特别识别的打破依赖关系的列表中。(此外,因为它没有被识别为清零习语,也就没有带来其他好处。)

有趣的是,最古老的P6设计(从PPro到Pentium III)并没有将xor零化视为一种打破依赖关系的方法,而只是将其视为一种避免部分寄存器停顿的惯用法。因此,在某些情况下,值得先使用mov指令,然后再使用xor零化指令来打破依赖关系,然后再次将高位设置为零,以确保EAX=AX=AL。
请参考Agner Fog在他的微架构PDF中的示例6.17。他说这也适用于P2、P3,甚至(早期的?)PM。链接博客文章的评论表示只有PPro存在这个疏忽,但我已经在Katmai PIII上进行了测试,@Fanael在Pentium M上进行了测试,我们都发现它无法打破一个延迟限制的imul链的依赖关系。不幸的是,这证实了Agner Fog的结果。 脚注1英特尔高级性能扩展(APX)引入了REX2和EVEX形式的整数指令,用于32个GPR以及常见指令的新的三操作数形式。最后是零扩展("零上位",又称ZU)形式的setcc r64。(指令总长度为6字节,在EVEX前缀中使用多余的位来编码寄存器目的地的传统与零扩展行为之间的差异。)

简而言之:

如果它确实能使你的代码更好或减少指令数,那么当然可以使用mov将寄存器清零,以避免影响标志位,只要它不会引入除代码大小以外的性能问题。避免破坏标志位是不使用xor的唯一合理理由,但有时候你可以在设置标志位之前进行异或清零,如果你有一个多余的寄存器。

setcc之前使用mov清零比在setcc之后使用movzx reg32, reg8(除非在英特尔上可以选择不同的寄存器)对延迟更好,但代码大小更大。


9
大多数算术指令 OP R,S 在乱序CPU中被迫等待寄存器R的内容被前面以寄存器R为目标的指令填充;这是一种数据依赖关系。关键点在于,Intel/AMD芯片有特殊的硬件来打破寄存器R上的必须等待数据依赖关系,当遇到XOR R,R时,而对于其他寄存器清零指令则不一定如此。这意味着XOR指令可以被安排立即执行,这也是为什么Intel/AMD建议使用它的原因。 - Ira Baxter
3
@IraBaxter:是的,为了避免任何混淆(因为我在SO上看到过这种误解),即使src是imm32、[mem]或另一个寄存器,mov reg, src也会打破OO CPU的依赖链。这种依赖关系的打破在优化手册中没有被提及,因为它不是一种特殊情况,只有当src和dest是同一个寄存器时才会发生。对于不依赖于其目标操作数的指令,它总是发生的(除了英特尔实现的popcnt/lzcnt/tzcnt对目标操作数存在虚假依赖的情况)。 - Peter Cordes
2
@Zboson:没有依赖关系的指令的“延迟”只有在流水线中出现气泡时才有意义。这对于mov-elimination很好,但对于清零指令而言,零延迟的好处只有在类似分支错误预测或I$ miss之后才会发挥作用,此时执行正在等待解码指令,而不是等待数据准备就绪。但是,是的,mov-elimination并不会使mov变得免费,只会变成零延迟。通常,“不占用执行端口”的部分并不重要。融合域吞吐量很容易成为瓶颈,特别是在混合负载或存储器操作中。 - Peter Cordes
4
啊,需要MIPS中的“零寄存器”时,好老的MIPS在哪里啊。 - hayalci
2
@ecm:我认为“both”的上下文在之前的编辑中丢失了;已经修复。感谢您指出。 - Peter Cordes
显示剩余28条评论

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