TL;DR 总结:xor same, same 是所有 CPU 的最佳选择。没有其他方法比它有任何优势,而且它至少比任何其他方法有一些优势。它是 Intel 和 AMD 官方推荐的,以及编译器的作用。在 64 位模式下,仍然使用xor r32, r32,因为writing a 32-bit reg zeros the upper 32。 xor r64, r64 浪费了一个字节,因为它需要一个 REX 前缀。
更糟糕的是,Silvermont 仅将 xor r32,r32 识别为 dep-break,而不是 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 的整数和 FP 之间的 2c 绕过延迟延迟通常不相关,因为乱序执行通常可以在新依赖链的开头隐藏它)。
在 SnB 系列微架构上,xor-zeroing 的风格甚至都不需要执行端口。在 AMD 和 Nehalem P6/Core2 之前的 Intel 上,xorps 和 pxor 的处理方式相同(作为向量整数指令)。
使用 128b 向量指令的 AVX 版本也会将 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.
见Is vxorps-zeroing on AMD Jaguar/Bulldozer/Zen faster with xmm registers than ymm?和
What is the most efficient way to clear a single or a few ZMM registers on Knights Landing?
半相关:Fastest way to set __m256 value to all ONE bits 和
Set all bits in CPU register to 1 efficiently 还涵盖了 AVX512 k0..7 掩码寄存器。 SSE/AVX vpcmpeqd 在许多方面都具有破坏性(尽管仍然需要一个 uop 来写入 1),但是用于 ZMM regs 的 AVX512 vpternlogd 甚至没有破坏性。在循环内部,请考虑从另一个寄存器复制,而不是使用 ALU uop 重新创建,尤其是使用 AVX512。
但是归零很便宜:在循环中对 xmm reg 进行异或归零通常与复制一样好,除了在某些 AMD CPU(Bulldozer 和 Zen)上,它们对向量 reg 进行了 mov-elimination 但仍需要 ALU uop 来写入异或归零的零。
在各种 uarches 上对 xor 之类的习语进行归零有什么特别之处
一些 CPU 将 sub same,same 识别为像 xor 这样的归零惯用语,但所有识别任何归零惯用语的 CPU 都会识别 xor。只需使用xor,这样您就不必担心哪个 CPU 识别哪个归零惯用语。
xor(与mov reg, 0 不同,是公认的归零习语)有一些明显和一些微妙的优势(汇总列表,然后我将对其进行扩展):
- 小于
mov reg,0 的代码大小。 (所有 CPU)
- 避免对后续代码的部分注册惩罚。 (英特尔 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 系列微架构上不使用执行单元进行异或的好处很小,但可以节省电力。 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 Michael Petch 链接(在对该问题的评论中)指出xor 在寄存器重命名阶段处理,不需要执行单元(未融合域中的零微指令),但错过了这一事实它仍然是融合域中的一个微指令。现代英特尔 CPU 可以每个时钟发出和淘汰 4 个融合域微指令。这就是每个时钟限制 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 的唯一优势是更短的编码。它还可能节省物理寄存器资源,但我还没有看到任何测试。
公认的归零习惯用法避免部分寄存器处罚在英特尔 CPU 上重命名部分寄存器与完整寄存器(P6 和 SnB 系列)分开。
xor 将将寄存器标记为上部归零,因此xor eax, eax / inc al / inc eax 避免了前 IvB CPU 通常的部分寄存器惩罚。即使没有xor,IvB 也只需要在修改高 8 位(AH)然后读取整个寄存器时进行合并,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 还确认 mov reg, 0 不被认为是归零习语,至少在 PIII 或 PM 等早期 P6 设计中是这样。如果他们用晶体管在后来的 CPU 上检测它,我会感到非常惊讶。
xor 设置标志,这意味着您在测试条件时必须小心。由于 setcc 很遗憾仅适用于 8 位目标,因此您通常需要注意避免部分注册的惩罚。
如果 x86-64 将已删除的操作码之一(如 AAM)重新用于 16/32/64 位 setcc r/m,并且谓词编码在 r 的源寄存器 3 位字段中,那就太好了/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 上都具有最佳性能(没有停顿、合并微指令或错误依赖项)。
如果您不想在标志设置指令之前进行异或操作,事情会变得更加复杂。例如你想在一个条件下分支,然后从相同的标志在另一个条件下设置cc。例如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 而不先清零是不好的,因为它们不会单独跟踪子寄存器的 deps。寄存器的旧值会有错误的依赖。当xor/test/setcc 不是一个选项时,使用mov reg, 0/setcc 进行归零/依赖破坏可能是最好的选择。
当然,如果您不需要setcc 的输出宽于 8 位,则无需将任何内容归零。但是,如果您选择的寄存器最近是长依赖链的一部分,请注意对 P6 / SnB 以外的 CPU 的错误依赖。 (如果您调用可能保存/恢复您正在使用的寄存器的一部分的函数,请注意导致部分 reg 停顿或额外的 uop。)
and 立即为零 并不是特殊情况,因为它独立于我所知道的任何 CPU 上的旧值,因此它不会破坏依赖链。它没有xor 的优势和许多缺点。
仅当您想要将依赖项作为延迟测试的一部分,但想要通过归零和添加来创建已知值时,它才对编写微基准测试有用。
有关微架构的详细信息,请参阅 http://agner.org/optimize/,包括哪些归零惯用语被识别为依赖关系破坏(例如,sub same,same 在某些但不是所有 CPU 上,而xor same,same 在所有 CPU 上都被识别。) mov 确实打破了对寄存器旧值的依赖链(无论源值如何,是否为零,因为这就是 mov 的工作方式)。 xor 仅在 src 和 dest 是同一个寄存器的特殊情况下打破依赖链,这就是为什么mov 被排除在特别识别的依赖破坏者列表之外。 (另外,因为它不被视为归零习语,还有其他好处。)
有趣的是,最古老的 P6 设计(PPro 到 Pentium III)没有将 xor-zeroing 识别为依赖关系破坏者,只是为了避免部分寄存器停顿,因此在某些情况下,值得使用 both mov 然后 xor-zeroing 以打破 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 上进行了测试,我们都发现它没有破坏对延迟绑定 @987654419 的依赖@ 链。不幸的是,这证实了 Agner Fog 的结果。
TL:DR:
如果它确实使您的代码更好或节省了指令,那么只要您不引入代码大小以外的性能问题,那么可以肯定的是,使用 mov 置零以避免接触标志。避免破坏标志是不使用xor 的唯一合理原因,但如果您有备用寄存器,有时您可以在设置标志的东西之前异或零。
mov-zero 在setcc 之前的延迟比movzx reg32, reg8 之后的延迟更好(在 Intel 上您可以选择不同的寄存器时除外),但代码大小更差。