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

What is the best way to set a register to zero in x86 assembly: xor, mov or and?

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

xorl   %eax, %eax
mov    [=10=], %eax
andl   [=10=], %eax

TL;DR 总结xor same, same所有CPU 的最佳选择。没有其他方法比它有任何优势,它至少比任何其他方法有一些优势。 Intel和AMD官方推荐的,编译器是做什么的。在64位模式下,仍然使用xor r32, r32,因为writing a 32-bit reg zeros the upper 32xor 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.  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 绕过整数和 FP 之间的延迟延迟通常不相关,因为乱序执行通常可以将其隐藏在新的依赖链的开头)。

在 SnB 系列微架构上,异或归零的任何一种风格都不需要执行端口。在 AMD 和 Nehalem 之前的 P6/Core2 Intel 上,xorpspxor 的处理方式相同(作为向量整数指令)。

使用 AVX 版本的 128b 向量指令也会将 reg 的上部置零,因此 vpxor xmm, xmm, xmm 是将 YMM(AVX1/AVX2) 或 ZMM(AVX512) 置零的不错选择,或任何未来的矢量扩展。不过,vpxor ymm, ymm, ymm 不需要任何额外的字节来编码,并且 运行 在 Intel 上是相同的,但在 Zen2 之前的 AMD 上速度较慢(2 微指令)。 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.

参见

半相关:
还包括 AVX512 k0..7 屏蔽寄存器。 SSE/AVX vpcmpeqd 在许多方面都是 dep-breaking(尽管仍然需要一个 uop 来写 1),但是 ZMM regs 的 AVX512 vpternlogd 甚至没有 dep-breaking。在循环内考虑从另一个寄存器复制,而不是使用 ALU uop 重新创建寄存器,尤其是对于 AVX512。

但是归零很便宜:在循环内对 xmm reg 进行异或归零通常与复制一样好,除了某些 AMD CPUs(Bulldozer 和 Zen),它们对向量 regs 进行了移动消除,但是仍然需要一个 ALU uop 来为异或归零写入零。


在各种 uarches 上将像 xor 这样的习语置零有什么特别之处

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

xor(与 mov reg, 0 不同,它是一个公认的归零惯用语)有一些明显的和一些微妙的优点(总结列表,然后我将扩展这些):

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

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


不使用执行单元 对英特尔 SnB 系列微体系结构上的 xor 的好处很小,但可以节省电量。它更可能对 SnB 或 IvB 有影响,它们只有 3 个 ALU 执行端口。 Haswell 和后来的有 4 个执行端口可以处理整数 ALU 指令,包括 mov r32, imm32,因此通过调度程序的完美决策(这在实践中并不总是发生),HSW 仍然可以维持每个时钟 4 微指令甚至当他们都需要ALU执行端口时。

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

Bruce Dawson's blog post that Michael Petch linked (in a comment on the question) points out that xor is handled at the register-rename stage without needing an execution unit (zero uops in the unfused domain), but missed the fact that it's still one uop in the fused domain. Modern Intel CPUs can issue & retire 4 fused-domain uops per clock. That's where the 4 zeros per clock limit comes from. Increased complexity of the register renaming hardware is only one of the reasons for limiting the width of the design to 4. (Bruce has written some very excellent blog posts, like his series on FP math and x87 / SSE / rounding issues,我强烈推荐)。


在 AMD Bulldozer 系列上 CPUsmov immediate 运行s 在相同的 EX0/EX1 整数执行端口上xormov reg,reg 也可以在 AGU0/1 上 运行,但这仅适用于寄存器复制,不适用于立即数设置。所以 AFAIK,在 AMD 上 xor 优于 mov 的唯一优势是更短的编码。说不定还能节省物理寄存器资源,不过我没见过测试。


已识别的归零习语避免部分寄存器惩罚 Intel CPUs 将部分寄存器与完整寄存器(P6 和 SnB 系列)分开重命名。

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

来自 Agner Fog 的微架构指南,第 98 页(Pentium M 部分,包括 SnB 在内的后续部分引用):

The processor recognizes the XOR of a register with itself as setting it to zero. A special tag in the register remembers that the high part of the register is zero so that EAX = AL. This tag is remembered even in a loop:

    ; 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 pg82): The processor remembers that the upper 24 bits of EAX are zero as long as you don't get an interrupt, misprediction, or other serializing event.

该指南的

pg82 还确认 mov reg, 0 不是 被认为是归零习惯用法,至少在 PIII 或 PM 等早期 P6 设计中是这样。如果他们在稍后 CPUs 上用晶体管检测它,我会感到非常惊讶。


xor 设置标志,这意味着您在测试条件时必须小心。遗憾的是,由于 setcc 仅适用于 8 位目标 ,您通常需要注意避免部分寄存器惩罚。

如果 x86-64 将删除的操作码之一(如 AAM)重新用于 16/32/64 位 setcc r/m,并在源寄存器 3 位中编码谓词,那就太好了r/m 字段的字段(其他一些单操作数指令将它们用作操作码位的方式)。但是他们没有那样做,而且这对 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/jlesete,并且您没有备用寄存器,或者您希望将 xor 完全排除在未采用的代码路径之外。

没有不影响标志的公认归零习惯用法,因此最佳选择取决于目标微体系结构。在 Core2 上,插入合并 uop 可能会导致 2 或 3 个周期停顿。它似乎在 SnB 上更便宜,但我没有花太多时间去衡量。使用 mov reg, 0 / setcc 会对较旧的 Intel CPUs 产生重大影响,并且在较新的 Intel 上仍然会更糟。

使用 setcc / movzx r32, r8 可能是 Intel P6 和 SnB 系列的最佳选择,如果您不能在标志设置指令之前进行异或零操作。这应该比异或归零后重复测试更好。 (甚至不考虑 sahf / lahfpushf / popf)。 IvB 可以消除 movzx r32, r8(即通过没有执行单元或延迟的寄存器重命名来处理它,如异或归零)。 Haswell 和后来只消除了常规 mov 指令,因此 movzx 需要一个执行单元并且具有非零延迟,使得 test/setcc/movzxxor/test/setcc,但至少仍然和 test/mov r,0/setcc 一样好(在旧的 CPUs 上更好)。

在 AMD/P4/Silvermont 上使用 setcc / movzx 而不先清零是不好的,因为它们不会单独跟踪子寄存器的 dep。寄存器的旧值会有错误的依赖。当 xor/test/setcc 不是一个选项时,使用 mov reg, 0/setcc 进行归零/依赖关系破坏可能是最好的选择。

当然,如果你不需要setcc的输出宽度超过8位,你不需要清零任何东西。但是,如果您选择的寄存器最近是长依赖链的一部分,请注意除 P6 / SnB 之外的 CPUs 的错误依赖。 (如果您调用的函数可能 save/restore 您正在使用的寄存器的一部分,请注意导致部分寄存器停顿或额外的 uop。)


and 立即为零 不是特例,因为我知道任何 CPUs 上的旧值都是独立的,所以它不会破坏依赖链。与 xor 相比,它没有任何优点,缺点也很多。

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


有关微架构的详细信息,请参见http://agner.org/optimize/,包括哪些归零惯用语被认为是依赖性破坏(例如sub same,same是一些但不是全部CPUs,而 xor same,same 被所有人识别。) mov 确实打破了对寄存器旧值的依赖链(无论源值是零还是不是,因为这就是 mov 有效)。 xor 只在 src 和 dest 是同一个寄存器的特殊情况下打破依赖链,这就是为什么 mov 被排除在 specially 认可的列表之外的原因依赖破坏者。 (另外,因为它不被认为是归零习语,所以还有其他好处。)

有趣的是,最古老的 P6 设计(PPro 到 Pentium III)没有xor-zeroing 识别为依赖性破坏者,仅作为避免部分寄存器停顿的归零习语 ,因此在某些情况下值得使用 both mov 然后 xor - 按此顺序清零以打破 dep,然后再次清零 + 设置高位为零的内部标记位,因此 EAX=AX=AL.

参见 Agner Fog 的示例 6.17。在他的 microarch pdf 中。他说这也适用于 P2、P3,甚至(早期?)PM。 A comment on the linked blog post 说只有 PPro 有这种疏忽,但我在 Katmai PIII 上测试过,@Fanael 在 Pentium M 上测试过,我们都发现它没有打破对延迟限制的依赖imul链。不幸的是,这证实了 Agner Fog 的结果。


TL:DR:

如果它确实使您的代码更好或节省指令,那么当然,使用 mov 为零以避免触及标志,只要您不引入除代码大小之外的性能问题。避免破坏标志是不使用 xor 的唯一合理原因,但有时如果你有备用寄存器,你可以在设置标志的东西之前进行异或零操作。

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