以下所有指令执行相同的操作:设置%eax
为零。哪种方法是最佳的(需要最少的机器周期)?
xorl %eax, %eax
mov $0, %eax
andl $0, %eax
以下所有指令执行相同的操作:设置%eax
为零。哪种方法是最佳的(需要最少的机器周期)?
xorl %eax, %eax
mov $0, %eax
andl $0, %eax
Answers:
TL; DR摘要:xor same, same
是所有CPU的最佳选择。没有其他方法比它具有任何优势,并且它比任何其他方法都具有至少某些优势。英特尔和AMD正式推荐使用它以及编译器的功能。在64位模式下,仍要使用xor r32, r32
,因为编写32位reg将高32位置零。 xor r64, r64
浪费一个字节,因为它需要一个REX前缀。
更糟糕的是,Silvermont仅将其识别xor r32,r32
为dep-breaking,而不是64位操作数大小。因此,即使由于将r8..r15清零而仍需要REX前缀时,也请使用xor r10d,r10d
,而不是xor r10,r10
。
GP整数示例:
xor eax, eax ; RAX = 0. Including AL=0 etc.
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
xor r10,r10 ; bad on Silvermont (not dep breaking), same as r10d everywhere else 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 al, al ; false dep on some CPUs, not a zeroing idiom. Use xor eax,eax
mov al, 0 ; only 2 bytes, and probably better than xor al,al *if* you need to leave the rest of EAX/RAX unmodified
最好将向量寄存器清零pxor xmm, xmm
。这通常是gcc所做的(甚至在与FP指令一起使用之前)。
xorps xmm, xmm
可以说得通。它比短1个字节pxor
,但是xorps
需要Intel Nehalem上的执行端口5,而pxor
可以在任何端口(0/1/5)上运行。(Nehalem在整数和FP之间的2c旁路延迟等待时间通常无关紧要,因为乱序执行通常会在新的依赖链开始时将其隐藏)。
在SnB系列微体系结构上,异或归零的形式都不需要执行端口。在AMD和预Nehalem的P6 / 2英特尔,xorps
和pxor
被处理的相同方式(如向量整数指令)。
使用128x向量指令的AVX版本也将reg的上部置零,因此vpxor xmm, xmm, xmm
将YMM(AVX1 / AVX2)或ZMM(AVX512)或任何将来的向量扩展置零是一个不错的选择。 vpxor ymm, ymm, ymm
不过,它不需要占用任何额外的字节来进行编码,并且在Intel上可以运行相同的字节,但是在Zen2(2微秒)之前的AMD上运行速度较慢。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.
请参见使用xmm寄存器比ymm在AMD Jaguar / Bulldozer / Zen上进行vxorps调零是否更快?而
什么是清除单个或骑士着陆几个ZMM寄存器的最有效方法是什么?
半相关:将__m256值设置为所有ONE位并将
CPU寄存器中的所有位有效设置为1的最快方法也涵盖了AVX512 k0..7
掩码寄存器。SSE / AVX vpcmpeqd
在许多方面都处在中断状态(尽管仍然需要写一个1),但是vpternlogd
ZMM regs的AVX512甚至没有中断。在循环内部,请考虑从另一个寄存器复制,而不是使用ALU uop(特别是使用AVX512)重新创建一个。
但是清零很便宜:在循环中对xmm reg进行零校正通常与复制一样好,除了在某些AMD CPU(Bulldozer和Zen)上具有对向量reg的消除功能外,但仍需要ALU uop才能为xor写入零。 -归零。
有些CPU sub same,same
像归零习惯一样识别xor
,但是所有识别任何归零习惯的CPU都可以识别xor
。只需使用即可xor
,您不必担心哪个CPU可以识别哪个清零习惯。
xor
(不同于,它是公认的归零惯用法mov reg, 0
)具有一些明显和微妙的优点(摘要列表,然后我将对其进行扩展):
mov reg,0
。(所有CPU)较小的机器代码大小(2个字节而不是5个字节)始终是一个优点:更高的代码密度导致更少的指令高速缓存未命中,以及更好的指令获取和可能的解码带宽。
在Intel SnB系列微体系结构上不对xor 使用执行单元的好处很小,但可以节省功耗。在SnB或IvB上更重要,因为它们只有3个ALU执行端口。Haswell及其更高版本具有4个执行端口,可以处理整数ALU指令,包括mov r32, imm32
,因此,通过调度程序的完美决策(实际上并不总是这样),即使它们都需要ALU,HSW仍可以每个时钟维持4 oups执行端口。
有关更多详细信息,请参见我对另一个有关将寄存器清零的问题的答案。
布鲁斯·道森(Bruce Dawson)在迈克尔·帕奇(Michael Petch)所链接的博客文章中(对问题的评论)指出,该问题xor
是在寄存器重命名阶段进行处理的,不需要执行单元(未融合域中的零微指令),但是却错过了它仍然是一个微指令的事实。在融合域中。现代的Intel CPU可以每个时钟发出和退出4个融合域uops。这就是每个时钟限制4个零的来源。增加了寄存器重命名的硬件复杂性只是其中的原因限制了设计的宽度为4一(布鲁斯已经写了一些非常优秀的博客文章,就像他在系列FP数学和的x87 / SSE /四舍五入的问题,这是我做的极力推荐)。
在AMD推土机系列CPU,mov immediate
在同一EX0 / EX1整数执行港口的运行xor
。 mov reg,reg
也可以在AGU0 / 1上运行,但这仅用于寄存器复制,而不用于立即数的设置。所以,据我所知,在AMD的唯一优势xor
在mov
越短的编码。它也可能节省物理寄存器资源,但是我还没有看到任何测试。
公认的清零习惯避免了对Intel CPU的部分寄存器惩罚,后者将部分寄存器与完整寄存器(P6和SnB系列)分开重命名。
xor
将标记寄存器为具有上部归零,所以xor eax, eax
/ inc al
/ inc eax
避免了通常的局部寄存器惩罚该预IVB CPU具有。即使没有xor
,仅当AH
修改高8位()然后读取整个寄存器时,IvB才需要合并uop ,而Haswell甚至将其删除。
摘自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
(来自pg82):处理器记住,只要您没有中断,错误预测或其他序列化事件,EAX的高24位就为零。
该指南的pg82还确认,至少在早期的P6设计(如PIII或PM)上,mov reg, 0
它不被视为归零习惯。如果他们花晶体管在以后的CPU上检测它,我会感到非常惊讶。
xor
设置flags,这意味着在测试条件时必须要小心。setcc
不幸的是,由于仅适用于8位目标地址,因此您通常需要注意避免部分注册处罚。
如果x86-64将已删除的操作码之一(例如AAM)重新定位为16/32/64位setcc r/m
,而谓词编码在r / m字段的源寄存器3位字段中,则效果很好(该方式其他一些单操作数指令将它们用作操作码位)。但是他们没有这样做,这对x86-32毫无帮助。
理想情况下,您应该使用xor
/设置标志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上都具有最佳性能(没有停顿,合并uops或错误的依赖项)。
当您不想在标志设置指令前进行异或运算时,事情会变得更加复杂。例如,您想在一个条件下分支,然后在相同条件下从另一个条件设置setcc。例如cmp/jle
,,sete
或者您没有备用寄存器,或者您希望将其完全xor
排除在未使用的代码路径之外。
没有公认的不会影响标记的清零习惯,因此最佳选择取决于目标微体系结构。在Core2上,插入合并的uop可能会导致2或3个周期的停顿。它在SnB上似乎更便宜,但是我并没有花费太多时间来进行测量。使用mov reg, 0
/ setcc
将对较旧的Intel CPU造成很大的损失,而对较新的Intel而言仍然会更糟。
如果您不能在标志设置指令之前进行异或为零,则使用setcc
/ movzx r32, r8
可能是Intel P6&SnB系列的最佳选择。这应该比在异或归零后重复测试更好。(甚至不考虑sahf
/ lahf
或pushf
/ popf
)。IvB可以消除movzx r32, r8
(即通过寄存器重命名来执行它,而无需执行单元或等待时间,例如异或归零)。Haswell及其以后只消除了常规mov
指令,因此movzx
占用了执行单元,并且具有非零延迟,使得test / setcc
/ movzx
比xor
/ test / 差setcc
,但仍然至少与test / mov r,0
/ 一样好setcc
(并且在较旧的CPU上要好得多)。
在AMD / P4 / Silvermont上,先使用setcc
/ movzx
而不进行归零是不好的,因为它们不会分别跟踪子寄存器的dep。寄存器的旧值将有错误的查询。当/ test / 不是选项时,使用mov reg, 0
/ setcc
进行归零/打破依赖关系可能是最好的选择。xor
setcc
当然,如果您不需要setcc
输出的宽度大于8位,则无需将任何内容归零。但是,请注意,如果您选择的寄存器最近是长依赖链的一部分,那么除了P6 / SnB以外,还要注意对CPU的错误依赖。(并且要小心,如果调用一个可能保存/恢复您正在使用的部分寄存器的函数,则会导致部分注册表停顿或额外的uop。)
and
在我所知道的任何CPU上,立即数都为零的情况都不是特殊情况,因为它与旧值无关,因此不会破坏依赖关系链。它没有优点,xor
也有很多缺点。
仅当您希望将依赖项作为等待时间测试的一部分,而又想通过归零和添加来创建一个已知值时,它才对编写微基准有用。
有关微体系结构的详细信息,请参见http://agner.org/optimize/,包括哪些调零成语被识别为依赖项中断(例如sub same,same
,在某些但不是所有CPU上,而在所有CPU xor same,same
上都被识别) mov
确实破坏了旧值的依赖项链。寄存器的值(不管源值是零还是零,因为这是这样mov
工作的)。 xor
仅在src和dest是同一寄存器的特殊情况下才中断依赖关系链,这就是为什么mov
不在特殊识别的依赖破坏程序列表中的原因。(此外,因为它不被视为归零习惯,还有其他好处。)
有趣的是,最古老的P6设计(通过的PPro奔腾III)不承认xor
-zeroing作为依赖断路器,仅作为用于避免局部寄存器档位的目的,调零成语,所以在某些情况下,这是值得使用二者 mov
,然后xor
-以该顺序清零以断开dep,然后再次清零+将内部标记位设置为高位为零,因此EAX = AX = AL。
参见Agner Fog的示例6.17。在他的microarch pdf中。他说,这也适用于P2,P3,甚至(早期?)PM。 在链接的博客文章上的评论说,只有PPro受到了监督,但是我已经在Katmai PIII上进行了测试,而@Fanael在Pentium M上进行了测试,我们都发现它并没有打破延迟的依赖关系。界imul
链。不幸的是,这证实了Agner Fog的结果。
如果它确实使您的代码更好,或保存了指令,那么mov
只要您不引入代码大小以外的性能问题,就可以肯定地使用0 来避免触及这些标志。避免破坏标志是不使用的唯一明智的原因xor
,但是如果您有备用寄存器,有时您可以在设置标志的值之前进行异或为零。
mov
-零提前setcc
比延迟更好movzx reg32, reg8
(在Intel可以选择不同的寄存器时除外),但延迟更短。
mov reg, src
也打破了OO CPU的dep链(无论src是imm32 [mem]
,还是其他寄存器)。在优化手册中没有提到此依赖断开,因为这不是特殊情况,只有在src和dest是同一寄存器时才会发生。它总是发生于那些不依赖于他们的DEST说明。(除了英特尔实施的popcnt/lzcnt/tzcnt
在目标上执行虚假dep的操作。)
mov
免费的,只有零延迟。“不占用执行端口”部分通常并不重要。尤其是,融合域的吞吐量很容易成为瓶颈。与负载或混合存储。
xor r64, r64
,不仅浪费一个字节。正如您所说,这xor r32, r32
是最佳选择,尤其是对于KNL。如果您想了解更多信息,请参见本micrarch手册中的第15.7节“特殊情况”。