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

时间:2015-11-12 07:55:01

标签: performance assembly optimization x86 micro-optimization

以下所有说明都做同样的事情:将%eax设置为零。哪种方式最佳(需要最少的机器周期)?

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

1 个答案:

答案 0 :(得分:188)

TL; DR摘要xor same, same是所有CPU的最佳选择。没有其他方法比它有任何优势,它至少比任何其他方法都有一些优势。它是英特尔和AMD正式推荐的。在64位模式下,仍然使用xor r32, r32,因为writing a 32-bit reg zeros the upper 32xor r64, r64浪费了一个字节,因为它需要一个REX前缀。

更糟糕的是,Silvermont只将xor r32,r32识别为dep-breaking而不是64位操作数。因此即使仍然需要REX前缀,因为您要将r8..r15归零,请使用xor r10d,r10d,而不是xor r10,r10

示例:

xor   eax, eax       ; RAX = 0
xor   r10d, r10d     ; R10 = 0
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
mov   eax, 0        ; doesn't touch FLAGS, but not faster and takes more bytes

通常最好使用pxor xmm, xmm对矢量寄存器进行归零。这通常是gcc的作用(甚至在使用FP指令之前)。

xorps xmm, xmm可以理解。它比pxor短一个字节,但xorps需要Intel Nehalem上的执行端口5,而pxor可以在任何端口(0/1/5)上运行。 (整数和FP之间的Nehalem 2c旁路延迟延迟通常不相关,因为无序执行通常可以在新的依赖链开始时隐藏它。)

在SnB系列微体系结构中,xor-zeroing的味道都不需要执行端口。在AMD和前Nehalem P6 / Core2 Intel上,xorpspxor的处理方式相同(向量整数指令)。

使用AVX版本的128b向量指令也会将reg的上半部分归零,因此vpxor xmm, xmm, xmm是将YMM(AVX1 / AVX2)或ZMM(AVX512)或任何未来向量归零的不错选择延期。但是,vpxor ymm, ymm, ymm不会对任何额外的字节进行编码,并且运行相同。 AVX512 ZMM归零需要额外的字节(对于EVEX前缀),因此应首选XMM或YMM归零。

有些CPU会将sub same,same识别为xor之类的归零习惯,但识别任何归零惯用语的所有CPU都会识别xor 。只需使用xor,您就不必担心哪个CPU会识别哪个归零习惯。

xor(与mov reg, 0不同,是一种公认​​的归零习语)有一些明显的和一些微妙的优点(摘要列表,然后我会扩展这些):

  • mov reg,0更小的代码大小。 (所有CPU)
  • 避免对以后的代码进行部分注册处罚。 (英特尔P6系列和SnB系列)。
  • 不使用执行单元,节省电力并释放执行资源。 (英特尔SnB系列)
  • 较小的uop(没有立即数据)在uop缓存行中留出空间,以便附近的指令在需要时借用。 (英特尔SnB系列)。
  • doesn't use up entries in the physical register file。 (英特尔SnB系列(和P4)至少可能是AMD,因为他们使用类似的PRF设计而不是像ROB P6系列微体系结构那样在ROB中保持寄存器状态。)

较小的机器代码大小(2个字节而不是5个)始终是一个优势:更高的代码密度导致更少的指令缓存未命中,更好的指令获取和潜在的解码带宽。

在英特尔SnB系列微架构上,不使用执行单元对xor的好处很小,但节省了电力。它更可能对SnB或IvB很重要,它只有3个ALU执行端口。 Haswell以及后来有4个执行端口可以处理整数ALU指令,包括mov r32, imm32,所以通过调度程序完美决策(实际上没有发生),HSW每个时钟仍然可以维持4个uop即使他们都需要执行端口。

有关详细信息,请参阅my answer on another question about zeroing registers

Michael Petch链接的

Bruce Dawson's blog post(在对该问题的评论中)指出xor在寄存器重命名阶段处理而不需要执行单元(在未融合域中为零uops),但是错过了它在融合领域仍然是一个uop的事实。现代英特尔CPU可以发布&每个时钟退出4个融合域uop。这是每个时钟限制的4个零来自哪里。寄存器重命名硬件的复杂性增加只是将设计宽度限制为4的原因之一。(布鲁斯写了一些非常优秀的博客文章,比如他在FP math and x87 / SSE / rounding issues上的系列文章,我强烈推荐)。

在AMD Bulldozer系列CPU上mov immediate在与xor相同的EX0 / EX1整数执行端口上运行。 mov reg,reg也可以在AGU0 / 1上运行,但这仅适用于寄存器复制,而不适用于即时设置。所以AFAIK,在AMD上xor超过mov的唯一优势是编码更短。它也可能节省物理注册资源,但我还没有看到任何测试。

在Intel CPU上识别归零成语避免部分寄存器处罚,它将部分寄存器与完整寄存器(P6和SnB系列)分开重命名。

xor 会将寄存器标记为将上部归零,因此xor eax, eax / inc al / inc eax会避免通常的部分寄存器IvB之前的CPU有的惩罚。即使没有xor,当修改高8位(AH)然后读取整个寄存器时,IvB只需要合并uop,Haswell甚至会删除它。

来自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
     

(来自第82页):处理器记住EAX的高24位只要为零   你不会得到中断,错误预测或其他序列化事件。

该指南的第82页也证实mov reg, 0 被认为是归零成语,至少在PIII或PM等早期P6设计中如此。如果他们花费晶体管在后来的CPU上检测它,我会感到非常惊讶。

xor设置标记,这意味着您在测试条件时必须小心。由于 setcc很遗憾只能使用8位目的地,因此您通常需要注意避免部分注册处罚。

如果x86-64将一个被删除的操作码(如AAM)重新用于16/32/64位setcc r/m,并且谓词编码在源寄存器3位字段中,那将是很好的。 r / m字段(一些其他单操作数指令将它们用作操作码位的方式)。但是他们没有做到这一点,无论如何这对x86-32都没有帮助。

理想情况下,您应该使用xor / set flags / setcc /读取完整注册:

...
call  some_func
xor     ecx,ecx    ; zero *before* the test
test    eax,eax
setnz   cl         ; cl = (some_func() != 0)
add     ebx, ecx   ; no partial-register penalty here

这在所有CPU上都具有最佳性能(没有停顿,合并uop或错误依赖)。

当你不想在标志设置指令之前xor时,事情会变得更复杂。例如你想在一个条件上分支,然后在同一个标​​志的另一个条件下setcc。例如cmp/jlesete,您要么没有备用注册,要么想要将xor完全保留在未采用的代码路径中。

没有公认的归零习语不会影响标志,因此最佳选择取决于目标微体系结构。在Core2上,插入合并uop可能会导致2或3个周期停顿。它在SnB上似乎更便宜,但我并没有花太多时间来测量。使用mov reg, 0 / setcc会对较旧的英特尔CPU造成重大损失,并且在较新的英特尔上仍会稍微恶化。

使用setcc / movzx r32, r8可能是英特尔P6& SnB系列,如果你可以在标志设置指令之前进行xor-zero。这应该比在xor-zeroing之后重复测试更好。 (不要考虑sahf / lahfpushf / popf)。 IvB可以消除movzx r32, r8(即使用寄存器重命名处理它而没有执行单元或延迟,如xor-zeroing)。 Haswell以及之后只删除了常规mov指令,因此movzx占用执行单元且延迟非零,使得test setcc / movzx比{{1}更差} / test / xor,但仍然至少和test / setcc / mov r,0一样好(并且在旧CPU上要好得多)。

在AMD / P4 / Silvermont上使用setcc / setcc首先没有归零是不好的,因为他们不会分别跟踪子寄存器的deps。寄存器的旧值会有一个错误的缺陷。当movzx / test / mov reg, 0不是选项时,使用setcc / xor进行归零/依赖性破坏可能是最佳选择。

当然,如果您不希望setcc的输出超过8位,则不需要将任何内容归零。但是,如果选择最近属于长依赖关系链的寄存器,请注意除P6 / SnB之外的CPU的错误依赖性。 (如果你打电话给可以保存/恢复你正在使用的部分注册的功能,请注意造成部分注册失效或额外的uop。)

setcc立即为零并不是特殊的,与我所知道的任何CPU上的旧值无关,所以它并不是真的。打破依赖链。它没有and的优势,也有许多缺点。

有关微型文档的详细信息,请参阅http://agner.org/optimize/,包括哪些归零习惯被识别为依赖性中断(例如,xor在某些但不是所有CPU上,而sub same,same在所有CPU上都被识别。){ {1}}确实打破了寄存器旧值的依赖关系链(无论源值是零还是零,因为xor same,same的工作原理如何)。 mov只在特殊情况下断开依赖链,其中src和dest是同一个寄存器,这就是为什么mov被排除在特别识别的依赖断层列表之外的原因。 (另外,因为它没有被认为是归零的成语,还带有其他好处。)

有趣的是,最古老的P6设计(PPro到奔腾III)并没有认识到xor - 归零作为依赖断路器,仅作为归零的惯用语避免部分寄存器停顿,所以在某些情况下值得使用两个。 (参见Agner Fog的例子6.17。在他的microarch pdf中。他说这也适用于P2,P3,甚至(早期?)PM。A comment on the linked blog post说只有PPro有这种疏忽,但是我已经在Katmai PIII上进行了测试,而@Fanael在Pentium M上进行了测试,我们都发现它并没有打破对延迟限制mov链的依赖。)

如果它确实使您的代码更好或保存说明,那么确保使用xor为零以避免触摸标记,只要您不引入除代码大小之外的性能问题。但是,避免使用clobbering标志是不使用imul的唯一合理理由。