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

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

xorl   %eax, %eax
mov    $0, %eax
andl   $0, %eax
75036 次浏览

DR 摘要 : xor same, same所有 CPU 的最佳选择。没有其他方法比它有任何优势,而且它至少比任何其他方法有一些优势。它是由 Intel 和 AMD 正式推荐的,以及编译器的功能。在64位模式下,仍然使用 xor r32, r32,因为 写一个32位的注册表,上面的32xor r64, r64是一个字节的浪费,因为它需要一个 REX 前缀。

更糟糕的是,Silvermont 只能识别 xor r32,r32为深度破坏,而不能识别64位操作数大小。

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 的2 c 旁路延迟时间通常不相关,因为乱序执行通常可以在一个新的依赖链的开始隐藏它)。

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

使用128b 矢量指令的 AVX 版本也将 reg 的上半部分归零,因此 vpxor xmm, xmm, xmm对于归零 YMM (AVX1/AVX2)或 ZMM (AVX512)或任何未来的矢量扩展是一个很好的选择。不过,vpxor ymm, ymm, ymm不需要任何额外的字节来编码,在 Intel 上运行相同的代码,但在 Zen2之前(2 uops)在 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 寄存器的 AMD Jaguar/Bulldozer/Zen 的 vxorps-zeroing 是否比 ymm 更快?
什么是最有效的方法来清除一个或几个 ZMM 寄存器骑士着陆?

半相关: 将 _ _ m256值设置为所有 ONE 位的最快方法
有效地将 CPU 寄存器中的所有位设置为1 也覆盖 AVX512 k0..7掩码寄存器。SSE/AVX vpcmpeqd在很多方面都是深度破坏的(尽管仍然需要一个 uop 来编写1) ,但是 AVX512 vpternlogd对于 ZMM 规则甚至不是深度破坏的。在循环内部,可以考虑从另一个寄存器复制,而不是使用 ALU up 重新创建寄存器,特别是 AVX512。

但是归零很便宜: 在循环中对 xmm reg 进行 xor-zeroing 通常和复制一样好,除了一些 AMD CPU (Bulldozer 和 Zen) ,它们对矢量 reg 具有移动消除功能,但是仍然需要一个 ALU uup 来为 xor-zeroing 写零。


像 xor 这样的成语在不同的 uarches 上归零有什么特别之处

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

xor(与 mov reg, 0不同,它是一个公认的归零习惯用法)有一些明显的和微妙的优势(总结列表,然后我将详细介绍这些优势) :

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

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


在 Intel SnB 系列微体系结构上,没有使用执行单位对 xor 的好处很小,但可以节省电源。它更有可能对只有3个 ALU 执行端口的 SnB 或 IvB 产生影响。Haswell 和之后的 HSW 有4个执行端口,可以处理包括 mov r32, imm32在内的整数 ALU 指令,因此,通过调度器的完美决策(这在实际中并不总是发生) ,HSW 仍然可以维持每个时钟4个运行时间,即使它们都需要 ALU 执行端口。

有关更多细节,请参见 我对另一个关于零寄存器的问题的回答

布鲁斯 · 道森(Bruce Dawson)在他的博客中指出,xor是在注册重命名阶段处理的,不需要执行单元(在未融合域中为0) ,但是他忽略了一个事实,那就是在融合域中它仍然是1。现代英特尔 CPU 可以发布和退休4融合域上位机每个时钟。这就是每个时钟限制4个零的来源。寄存器重命名硬件复杂度的增加仅仅是将设计宽度限制为4的原因之一。(Bruce 写过一些非常优秀的博客文章,比如他的 FP 数学和 x87/SSE/舍入问题系列,我强烈推荐)。


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


英特尔 CPU 上可识别的归零习惯用法 避免部分注册罚款,它将部分寄存器与完整寄存器分开重命名(P6和 SnB 系列)。

xor将寄存器标记为上部零件为零,因此 xor eax, eax/inc al/inc eax避免了前 IvB CPU 所具有的通常的部分寄存器损失。即使没有 xor,IvB 也只需要在修改高8位(AH)然后读取整个寄存器时进行合并操作,Haswell 甚至删除了这一操作。

来自 Agner Fog 的微拱指南,pg 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位是0,只要 不会出现中断、错误预测或其他序列化事件。

该指南的 pg82也证实了 mov reg, 0没有,至少在 PIII 或 PM 这样的早期 P6设计中,没有被认为是一种归零习语。如果他们用晶体管在以后的 CPU 上检测它,我会非常惊讶。


xor设置标志 ,这意味着在测试条件时必须小心。自 不幸的是,setcc只有8位目标可用以来,您通常需要注意避免部分注册的处罚。

如果 x86-64将一个删除的操作码(如 AAM)改为16/32/64位 setcc r/m,并将谓词编码在 r/m 字段的源寄存器3位字段中(其他单操作数指令将它们用作操作码位的方式) ,那就太好了。但是他们没有这样做,而且这对 x86-32也没有帮助。

理想情况下,应该使用 xor/set 标志/setcc/read 完整寄存器:

...
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 或错误依赖)。

当您不希望在标志设置指令 之前使用 xor 时,情况会更加复杂。例如,您希望在一个条件上进行分支,然后在来自相同标志的另一个条件上进行 setcc 分支。例如 cmp/jlesete,而且您要么没有备用寄存器,要么希望将 xor完全排除在未采用的代码路径之外。

没有公认的不影响标志的归零习惯用法,因此最佳选择取决于目标微架构。在 Core2上,插入合并操作可能会导致2或3个周期停滞。在 SnB 上似乎更便宜,但我没有花太多时间去测量。使用 mov reg, 0/setcc将会对老的 Intel CPU 造成很大的损失,而对于新的 Intel 来说则会更糟糕。

如果在标志设置指令之前不能使用 xor-zero,那么使用 setcc/movzx r32, r8可能是 Intel P6和 SnB 系列的最佳选择。这应该比在 xor-zeroing 之后重复测试要好。(甚至不要考虑 sahf/lahfpushf/popf)。IvB 可以消除 movzx r32, r8(即使用没有执行单元或延迟的寄存器重命名来处理它,比如 xor-zeroing)。Haswell 和之后的版本只是消除了常规的 mov指令,所以 movzx采用一个执行单元并且具有非零延迟,这使得 test/setcc/movzxmovzx r32, r81/test/setcc差,但是至少和 test/movzx r32, r83/setcc一样好(在旧的 CPU 上更好)。

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

当然,如果您不需要 setcc的输出宽度超过8位,那么就不需要为零。但是,如果您选择的寄存器最近是一个长依赖链的一部分,则要小心对 P6/SnB 以外的 CPU 的错误依赖。(如果调用的函数可能会保存/恢复正在使用的寄存器的一部分,则要小心引起部分 reg 停顿或额外的 uup。)


直接为0 and与我所知的任何 CPU 上的旧值都不是独立的,因此它不会打破依赖链。与 xor相比,它没有优点,也有很多缺点。

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


请参阅 http://agner.org/optimize/了解 microarch 的详细信息 ,包括哪些归零习惯用法被识别为依赖破坏(例如,sub same,same在某些 CPU 上,但不是所有 CPU 上,而 xor same,same在所有 CPU 上都被识别)mov确实打破了寄存器旧值上的依赖链(不管源值是否为零,因为这就是 mov的工作方式)。xor只在 src 和 est 是相同寄存器的特殊情况下打破依赖链,这就是为什么 mov被排除在 特别是识别的依赖断开器列表之外。(另外,因为它不被认为是一个归零习语,还有其他的好处。)

有趣的是,最古老的 P6设计(PPro 到 Pentium III) 没有xor零点识别为依赖破坏器,只是作为一种零点习惯用法,目的是避免部分寄存器停顿 ,所以在某些情况下,值得使用 都有 mov,然后 xor零点,以便打破深度,然后再次零 + 设置内部标签位高位为零,所以 EAX = AX = AL。

参见 Agner Fog 的例子6.17。在他的微型拱门 pdf 里。他说,这也适用于 P2,P3,甚至(早期?)首相。对链接博客文章的评论说只有 PPro 有这种疏忽,但我在 Katmai PIII 测试过,@Fanael 在奔腾 M 上测试过,我们都发现它没有打破对延迟限制的 imul链的依赖。不幸的是,这证实了 Agner Fog 的结果。


译者:

如果它确实使您的代码更好或者保存了指令,那么当然,使用 mov为零以避免触及标志,只要您不引入代码大小以外的性能问题。避免重击标志是不使用 xor的唯一合理原因,但有时如果您有一个备用寄存器,您可以在设置标志的事情之前使用 xor-zero。

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