当前位置: 首页 > 知识库问答 >
问题:

在x86汇编中将寄存器设置为零的最佳方式是xor、mov还是AND?

夏炎彬
2023-03-14

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

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

共有1个答案

许兴文
2023-03-14

TL;DR摘要:异或相同,same是所有CPU的最佳选择。没有其他方法比它有任何优势,它至少比任何其他方法有一些优势。它是由英特尔和AMD官方推荐的,以及编译器所做的。在64位模式下,仍然使用异或r32,r32,因为写32位reg会使上面的32为零。异或r64,r64浪费一个字节,因为它需要一个REX前缀。

更糟糕的是,Silvermont只识别异或r32、r32为dep中断,而不是64位操作数大小。因此,即使由于将r8..r15归零而仍然需要REX前缀,也要使用异或r10d,r10d,而不是异或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

使用128B向量指令的AVX版本也会使reg的上部归零,因此vpxor xmm,xmm,xmm是归零YMM(AVX1/AVX2)或ZMM(AVX512)或任何未来的向量扩展的好选择。VPXOR ymm,ymm,ymm不需要任何额外的字节来编码,在Intel上运行相同,但在Zen2之前的AMD上运行更慢(2个uops)。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寄存器对AMD捷豹/推土机/Zen进行VXORPS-Zering比YMM更快吗?并且
在骑士登陆时清除一个或几个ZMM注册表的最有效方法是什么?

但是归零很便宜:XOR归零循环中的xmm reg通常和复制一样好,除了一些AMD CPU(推土机和Zen)之外,它们对向量reg有MOV消除,但仍然需要一个ALU uop来写零以进行XOR归零。

有些CPU将sub same,same识别为像XOR这样的归零习语,但所有识别任何归零习语的CPU都识别XOR。只需使用XOR,这样您就不必担心哪个CPU能识别哪个归零习惯用法了。

XOR(与MOV reg不同,0)是一个公认的归零习惯用法)有一些明显和微妙的优点(摘要列表,然后我将展开这些优点):

  • mov reg小的代码大小,0。(所有CPU)
  • 避免了对以后代码的部分注册惩罚。(Intel P6系列和SNB系列)。
  • 不使用执行单元,节省了电源并释放了执行资源。(英特尔SNB系列)
  • 较小的uop(没有即时数据)在uop缓存行中留出空间,以便在需要时借用附近的指令。(Intel SNB系列)。
  • 不会用完物理寄存器文件中的条目。(至少Intel SNB系列(和P4),也可能是AMD,因为它们使用类似的PRF设计,而不是像Intel P6系列微架构那样在ROB中保持寄存器状态。)

较小的机器代码大小(2字节而不是5字节)总是一个优点:更高的代码密度导致更少的指令缓存丢失,更好的指令提取和潜在的解码带宽。

在Intel SNB系列微架构上不使用执行单元进行异或的好处很小,但节省了电力。在只有3个ALU执行端口的SnB或IvB上,这可能更重要。Haswell和后来有4个执行端口,可以处理整数ALU指令,包括MOV r32、IMM32,因此通过调度器的完美决策(实际上并不总是这样),HSW仍然可以在每个时钟支持4个uops,即使它们都需要ALU执行端口。

Michael Petch链接的Bruce Dawson的博客文章(在对这个问题的评论中)指出,XOR在register-rename阶段处理,而不需要执行单元(未使用的域中有零个uop),但忽略了在融合的域中仍然有一个uop这一事实。现代英特尔CPU可以发出和退出4个融合域UOP每时钟。这就是每个时钟4个零的限制来自哪里。寄存器重命名硬件复杂性的增加只是将设计宽度限制在4的原因之一。(Bruce写了一些非常优秀的博客文章,比如他关于FP数学和x87/SSE/舍入问题的系列文章,我非常推荐这些文章)。

在AMD推土机系列CPU上,MOV immediate运行在与XOR相同的ex0/ex1整数执行端口上。MOV reg,reg也可以在AGU0/1上运行,但这仅用于寄存器复制,不能用于从即时设置。所以AFAIK,在AMD上,XOR相对于MOV的唯一优势是编码更短。它还可能节省物理注册资源,但我没有看到任何html" target="_blank">测试。

识别归零习惯用法避免了英特尔CPU的部分寄存器惩罚,后者将部分寄存器与完整寄存器(P6和SnB系列)分开重命名。

XOR将标记寄存器的上部已归零,因此XOR eax、eax/Inc al/Inc eax避免了IVB前CPU常见的部分寄存器损失。即使没有XOR,IvB也只需要在修改高8bits(AH)然后读取整个寄存器时进行合并uop,Haswell甚至将其删除。

从Agner Fog的microarch guide,pg 98(奔腾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还确认MOV reg,0不被识别为零化习惯用法,至少在早期的P6设计中,如PIII或PM。如果他们在以后的CPU上花费晶体管来检测它,我会非常惊讶。

...
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个循环停顿。它似乎比瑞士央行便宜,但我没有花太多时间试图衡量。使用MOV reg,0/setcc对较旧的Intel CPU会有很大的影响,但在较新的Intel CPU上会有一些影响。

当然,如果不需要setcc的输出大于8位,则不需要为零。但是,如果您选择的寄存器最近是长依赖链的一部分,请小心对P6/SnB以外的CPU的错误依赖。(如果您调用的函数可能保存/恢复您正在使用的部分寄存器,请小心导致部分reg停滞或额外的uop。)

直接为零的在我所知道的任何CPU上都没有特殊的大小写,因为它与旧值无关,所以它不会破坏依赖链。与XOR相比,它没有优势,还有许多缺点。

只有当您想要依赖项作为延迟测试的一部分,但想要通过归零和添加来创建已知值时,它才对编写微基准测试有用。

有趣的是,最早的P6设计(从PPro到Pentium III)没有将XOR-zeroing识别为依赖关系中断器,而是将其识别为一种零化习惯用法,目的是避免部分寄存器停滞,因此在某些情况下,值得同时使用MOVXOR-zeroing来中断dep,然后再次为零+设置内部标记位,使高位为零,因此EAX=AX=AL。

参见Agner Fog的示例6.17。在他的微弓PDF里。他说这也适用于P2、P3,甚至(早期?)链接的博客文章中的一个评论说,只有PPro有这个疏忽,但我在Katmai PIII上测试过,@fanael在Pentium M上测试过,我们都发现它没有破坏对有延迟限制的imul链的依赖。不幸的是,这证实了Agner Fog的结果。

如果它真的使代码更好或保存指令,那么当然可以使用mov为零以避免触及标志,只要不引入除代码大小以外的性能问题。避免打乱标志是不使用XOR的唯一合理的原因,但是如果有备用寄存器,有时可以在设置标志的事情之前进行XOR-零。

mov-setcc前面为零的延迟比movzx reg32、reg8后面为好(在Intel上例外,您可以选择不同的寄存器),但代码大小更差。

 类似资料:
  • 本文向大家介绍Intel x86 Assembly& Microarchitecture 调零寄存器,包括了Intel x86 Assembly& Microarchitecture 调零寄存器的使用技巧和注意事项,需要的朋友参考一下 示例 将寄存器归零的明显方法是MOV在0—例如: 请注意,这是一个5字节指令。 如果您愿意破坏标志(MOV从不影响标志),则可以使用XOR指令将寄存器与其自身按位异

  • 在wikipedia x86调用约定中,它说对于Microsoft x64调用约定: 寄存器RBX、RBP、RDI、RSI、RSP、R12、R13、R14和R15被视为非易失性(被叫方保存)。 但对于System V AMD64 ABI: 如果被调用方希望使用寄存器RBX、RBP和R12-R15,则必须在将控制权返回给调用方之前恢复它们的原始值。 我的问题是,在不同的平台上调用约定是不是不同的?(

  • 我正在为一个操作系统分配编写内联汇编代码。我有一些关于内联汇编和gcc编译器将其转换为机器代码的问题。 null

  • 我很想知道在x86架构上实现大容量内存拷贝的最佳方法。我意识到这取决于机器的特定特性。主要目标是过去4-5年制造的典型台式机。 我知道在过去,带有REPE的MOVSD名义上是最快的方法,因为您可以一次移动4个字节,但我读到现在MOVSB同样快速且编写更简单,所以您不妨进行字节移动,忘记4字节移动的复杂性。 一个围绕的问题是MOVxx指令是否值得。如果CPU的运行速度比内存总线快得多,那么使用CIS

  • 具体是: 注意,我关心的是旧的x86 linux CPU,而不是现代的x86_64 CPU,那里的分段工作方式不同。

  • 2. x86的寄存器 x86的通用寄存器有eax、ebx、ecx、edx、edi、esi。这些寄存器在大多数指令中是可以任意选用的,比如movl指令可以把一个立即数传送到eax中,也可传送到ebx中。但也有一些指令规定只能用其中某个寄存器做某种用途,例如除法指令idivl要求被除数在eax寄存器中,edx寄存器必须是0,而除数可以在任意寄存器中,计算结果的商数保存在eax寄存器中(覆盖原来的被除数