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

vwoqyblh  于 2023-05-29  发布在  其他
关注(0)|答案(1)|浏览(135)

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

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

1aaf6o9v1#

TL;DR摘要xor same, same所有CPU的最佳选择。没有其他方法比它有任何优势,它至少比任何其他方法有一些优势。它是Intel和AMD官方推荐的,编译器是做什么的。在64位模式下,仍然使用xor r32, r32,因为writing a 32-bit reg zeros the upper 32xor r64, r64是一个字节的浪费,因为它需要一个雷克斯前缀。

更糟糕的是,Silvermont只将xor r32,r32识别为深度中断,而不是64位操作数大小。因此,即使因为要将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的2c旁路延迟integer和FP之间的延迟通常是不相关的,因为乱序执行通常可以在新依赖链的开始隐藏它)。
在SnB系列微体系结构上,异或归零甚至都不需要执行端口。在AMD和Nehalem P6/Core 2之前的Intel上,xorpspxor的处理方式相同(作为向量整数指令)。
使用AVX版本的128 b向量指令也会将reg的上部置零,因此vpxor xmm, xmm, xmm是将YMM(AVX 1/AVX 2)或ZMM(AVX 512)或任何未来的向量扩展置零的理想选择。vpxor ymm, ymm, ymm不需要任何额外的字节来编码,虽然,在英特尔上运行相同,但在Zen 2之前的AMD上运行较慢(2 uops)。AVX 512 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.

参见Is vxorps-zeroing on AMD Jaguar/Bulldozer/Zen faster with xmm registers than ymm?
What is the most efficient way to clear a single or a few ZMM registers on Knights Landing?
半相关:Fastest way to set __m256 value to all ONE bits
有效地将CPU寄存器中的所有位设置为1还涵盖AVX 512 k0..7掩码寄存器。SSE/AVX vpcmpeqd在很多情况下都是深度破坏的(尽管仍然需要一个uop来写1),但是ZMM规则的AVX 512 vpternlogd甚至都不是深度破坏的。在循环内部,考虑从另一个寄存器复制,而不是使用ALU uop重新创建寄存器,特别是使用AVX 512。
但是归零很便宜:在循环中对xmm reg进行异或置零通常与复制一样好,除了在一些AMD CPU(Bulldozer和Zen)上,这些CPU具有针对向量reg的移动消除,但仍然需要ALU uop来写入零以进行异或置零。

在不同的uarches上将xor等习惯用法归零有什么特别之处

一些CPU将sub same,same识别为像xor这样的归零习惯用法,但是所有识别任何归零习惯用法的CPU都识别xor。只需使用xor,这样就不必担心哪个CPU识别哪个归零习惯用法。
xor(作为一个公认的归零习惯用法,与mov reg, 0不同)有一些明显的优点和一些微妙的优点(总结列表,然后我将详细介绍这些优点):

  • 代码大小小于mov reg,0。(所有CPU)
  • 避免了后续代码的部分寄存器损失。(Intel P6系列和SnB系列)。
  • 不使用执行单元,从而节省电力并释放执行资源。(英特尔SnB系列)
  • 较小的uop(没有立即数据)在uop高速缓存行中为附近的指令留出空间,以便在需要时借用。(英特尔SnB系列)。
  • doesn't use up entries in the physical register file。(至少Intel SnB系列(和P4),可能还有AMD,因为它们使用类似的PRF设计,而不是像Intel P6系列微体系结构那样将寄存器状态保存在ROB中。)
    较小的机器码大小(2字节而不是5字节)始终是一个优势:更高的代码密度导致更少的指令缓存未命中,以及更好的指令提取和潜在的解码带宽。

在英特尔SnB系列微体系结构上不使用执行单元进行异或运算的好处很小,但可以节省功耗。这在SnB或IvB上更有可能,因为它们只有3个ALU执行端口。Haswell和更高版本有4个执行端口可以处理整数ALU指令,包括mov r32, imm32,因此通过调度器的完美决策(实际上并不总是发生),HSW仍然可以维持每个时钟4个uops,即使它们都需要ALU执行端口。
请参阅my answer on another question about zeroing registers了解更多详细信息。
Michael Petch链接的Bruce Dawson's blog post(在对该问题的评论中)指出,xor在寄存器重命名阶段处理,而不需要执行单元(未融合域中的零个uop),但忽略了它在融合域中仍然是一个uop的事实。现代英特尔CPU每个时钟可以发出和退役4个融合域uop。这就是每个时钟4个零的限制的来源。寄存器重命名硬件的增加的复杂性仅仅是将设计的宽度限制为4的原因之一。(布鲁斯写了一些非常优秀的博客文章,比如他关于FP math and x87 / SSE / rounding issues的系列文章,我强烈推荐)。

在AMD推土机系列CPU上,mov immediatexor在相同的EX 0/EX 1整数执行端口上运行。mov reg,reg也可以在AGU 0/1上运行,但这仅适用于寄存器复制,不适用于立即数设置。所以AFAIK,在AMD上,xor相对于mov的唯一优势是编码更短。它还可能保存物理寄存器资源,但我还没有看到任何测试。

公认的归零习惯用法避免了英特尔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在内的后续部分引用):
处理器将寄存器与自身的XOR识别为将其设置为零。寄存器中的一个特殊标记记住寄存器的高电平部分为零,因此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

(from pg 82):只要没有发生中断、预测错误或其他串行化事件,处理器就会记住EAX的高24位为0。
该指南的第82页也证实了mov reg, 0 * 不 * 被认为是归零习惯用法,至少在早期的P6设计中,如PIII或PM。如果他们在后来的CPU上花费晶体管来检测它,我会非常惊讶。

xor设置标志,这意味着在测试条件时必须小心。由于**setcc只适用于8位目标**,因此通常需要注意避免部分寄存器损失。

如果x86-64将其中一个删除的操作码(如AAM)重新用于16/32/64位setcc r/m,并将 predicate 编码在r/m字段的源寄存器3位字段中(其他一些单操作数指令将其用作操作码位的方式),那就太好了。但是他们没有这样做,而且这对x86-32也没有帮助。
理想情况下,应该使用xor/ set flags /setcc/ read full register:

...
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上都具有最佳性能(没有停顿、合并uop或假依赖项)。(如果条件是ebx += (eax != 0),有一些技巧,比如cmp eax, 1; sbb ebx, -1使用进位标志与adcsbb直接相加或相减,而不是将其具体化为0/1整数,正如@l4m2在评论中指出的那样。执行sub eax, 42(或莱亚到另一个reg)/cmp eax,1/sbb甚至可能是值得的。特别是如果在设置FLAGS之前很难安排异或零,因为cmp/setcc/movzx/add在延迟的关键路径上有所有4个操作。

当你不想在一个标志设置指令之前进行异或运算时,事情会变得更加复杂。例如,你想在一个条件下分支,然后在另一个条件下从相同的标志中setcc。例如cmp/jlesete,并且您要么没有备用寄存器,要么希望将xor完全排除在未采用的代码路径之外。

没有公认的不影响标志的归零习惯用法,因此最佳选择取决于目标微体系结构。在核心2上,插入合并uop可能会导致2或3个周期的停顿。它在SnB上更便宜,最坏的情况下是1个周期,而Haswell and later don't rename partial registers separately from full regs。在最新的CPU上使用mov reg, 0/setcc可能是最好的,但在较旧的Intel CPU(Nehalem和更早版本)上会有很大的损失。在较新的CPU上,它接近于xor-zeroing,但代码大小比movzx差。
如果不能在标志设置指令之前执行异或零运算,那么使用setcc/movzx r32, r8可能是Intel P6的最佳替代方案。这应该比异或归零后重复测试更好。(甚至不要考虑sahf/lahfpushf/popf)。IvB和更高版本(Ice Lake除外)可以消除movzx r32, r8(即用寄存器重命名来处理它,而没有执行单元或等待时间,如XOR置零)。AMD Zen家族只能消除常规的mov指令,因此movzx需要一个执行单元,并且具有非零延迟,使得test/setcc/movzxxor/test/setcc更差。也比test/mov r,0/setcc差(但在具有部分寄存器停顿的旧版Intel CPU上要好得多)。
在AMD/P4/Silvermont上使用setcc/movzx而不先归零是不好的,因为它们不单独跟踪子寄存器的deps。在寄存器的旧值上会有一个错误的dep。**当xor/test/setcc不是一个选项时,使用mov reg, 0/setcc进行归零/依赖关系破坏可能是最好的选择。**至少对于“热”代码,这是重要延迟链的一部分。否则使用movzx来保存一点代码大小。

当然,如果你不需要setcc的输出宽度超过8位,你不需要把任何东西归零。但是,如果您选择的寄存器最近是一个长依赖链的一部分,请注意对P6 / SnB以外的CPU的错误依赖。(如果你调用一个可能保存/恢复你正在使用的寄存器的函数,要小心导致部分寄存器停止或额外的uop。

and带有立即零不是特殊情况,因为它独立于我所知的任何CPU上的旧值,所以它不会破坏依赖链。它与xor相比没有任何优势,但有许多缺点。

它只在编写微基准测试时有用,当你 * 希望 * 依赖项作为延迟测试的一部分,但希望通过归零和加法创建一个已知值时。

请参阅http://agner.org/optimize/了解微结构细节,包括哪些归零习惯用法被识别为依赖关系破坏(例如sub same,same在某些CPU上,但不是所有CPU上,而xor same,same在所有CPU上都被识别。)mov确实打破了寄存器旧值的依赖链(无论源值是零还是零,因为mov就是这样工作的)。xor仅在src和dest是同一寄存器的特殊情况下破坏依赖链,这就是为什么mov被排除在 * 特别 * 识别的依赖破坏者列表之外。(另外,因为它不被认为是一个归零习惯用法,还有其他好处。)
有趣的是,最古老的P6设计(PPro到Pentium III) 没有 * 将xor-调零视为依赖性破坏者,仅作为一种调零习惯用法,以避免部分寄存器停滞*,因此在某些情况下,值得使用 * movxor-按该顺序调零以打破dep,然后再次调零+设置内部标记位,使高位为零,因此EAX=AX= AL。

参见Agner Fog的示例6.17。在他的微型拱门PDF中。他说,这也适用于P2,P3,甚至(早期?链接的博客文章上的评论说,只有PPro有这个疏忽,但我在Katmai PIII上测试过,@Fanael在奔腾M上测试过,我们都发现它没有打破延迟绑定imul链的依赖关系。不幸的是,这证实了Agner Fog的结果。

TL:DR:

如果它真的使代码更好或节省了指令,那么当然,只要不引入代码大小以外的性能问题,就可以用mov为零,以避免触及标志。避免破坏标志是不使用xor的唯一合理原因,但有时候如果有备用寄存器,可以在设置标志之前进行异或零操作。
mov-setcc之前的零比movzx reg32, reg8之后的延迟更好(在英特尔上,当您可以选择不同的寄存器时除外),但代码大小更差。

相关问题