以下所有说明都做同样的事情:将%eax
设置为零。哪种方式最佳(需要最少的机器周期)?
xorl %eax, %eax
mov $0, %eax
andl $0, %eax
答案 0 :(得分:188)
TL; DR摘要:xor same, same
是所有CPU的最佳选择。没有其他方法比它有任何优势,它至少比任何其他方法都有一些优势。它是英特尔和AMD正式推荐的。在64位模式下,仍然使用xor r32, r32
,因为writing a 32-bit reg zeros the upper 32。 xor 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上,xorps
和pxor
的处理方式相同(向量整数指令)。
使用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)较小的机器代码大小(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参考):
该指南的第82页也证实处理器将自身的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位只要为零 你不会得到中断,错误预测或其他序列化事件。
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/jle
,sete
,您要么没有备用注册,要么想要将xor
完全保留在未采用的代码路径中。
没有公认的归零习语不会影响标志,因此最佳选择取决于目标微体系结构。在Core2上,插入合并uop可能会导致2或3个周期停顿。它在SnB上似乎更便宜,但我并没有花太多时间来测量。使用mov reg, 0
/ setcc
会对较旧的英特尔CPU造成重大损失,并且在较新的英特尔上仍会稍微恶化。
使用setcc
/ movzx r32, r8
可能是英特尔P6& SnB系列,如果你可以在标志设置指令之前进行xor-zero。这应该比在xor-zeroing之后重复测试更好。 (不要考虑sahf
/ lahf
或pushf
/ 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
的唯一合理理由。