使用 CMP reg,0 与 OR reg,reg 测试寄存器是否为零?

Test whether a register is zero with CMP reg,0 vs OR reg,reg?

使用以下代码执行速度是否有差异:

cmp al, 0
je done

及以下:

or al, al
jz done

我知道 JE 和 JZ 指令是相同的,而且使用 OR 可以减少一个字节的大小。但是,我也很关心代码速度。逻辑运算符似乎比 SUB 或 CMP 更快,但我只是想确定一下。这可能是大小和速度之间的权衡,或者双赢(当然代码会更不透明)。

这取决于确切的代码序列,具体是哪个CPU,以及其他因素。

or al, al, 的主要问题是它 "modifies" EAX,这意味着以某种方式使用 EAX 的后续指令可能会停止,直到该指令完成. 请注意,条件分支 (jz) 也取决于指令,但 CPU 制造商做了大量工作(分支预测和推测执行)来缓解这种情况。另请注意,理论上 CPU 制造商可以设计一个 CPU 来识别 EAX 在这种特定情况下没有改变,但有数百种特殊情况和承认其中大部分的好处太少了。

cmp al,0 的主要问题是它稍大,这可能意味着较慢的指令 fetch/more 缓存压力,并且(如果它是一个循环)可能意味着代码不再适合一些 CPU 的 "loop buffer".

正如 Jester 在评论中指出的那样; test al,al 避免了这两个问题 - 它小于 cmp al,0 并且不修改 EAX.

当然(取决于具体顺序)AL中的值一定来自某处,如果它来自适当设置标志的指令,则可能修改代码以避免稍后使用另一条指令再次设置标志。

,性能有差异

将寄存器与零进行比较的最佳选择是test reg, reg。它设置 FLAGS 的方式与 cmp reg,0 相同,并且至少与任何其他方式一样快 1,代码量更小。

(更好的是当 ZF 已经被设置 reg 的指令适当地设置时,所以你可以直接分支、setcc 或 cmovcc。例如, 通常看起来像 dec ecx / jnz .loop_top。大多数 x86 整数指令“根据结果设置标志”,如果输出为 0。包括 ZF=1。

or reg,reg 不能 with a JCC 进入任何现有 x86 CPUs 上的单个 uop,并为以后读取 reg 的任何内容增加延迟,因为它重写了值存入寄存器。 cmp 的缺点通常只是代码大小。

脚注 1:可能存在例外,但仅限于过时的 P6 系列 CPUs(Intel 直至 Nehalem,在 2011 年被 Sandybridge 系列取代)。请参阅下文,了解如何通过将相同的值重写到寄存器中来避免寄存器读取停顿。其他微体系结构系列没有这样的停顿,并且 or 永远不会超过 test


FLAGS results of test reg,reg/and reg,reg/or reg,reg
在所有情况下(AF 除外)都与 cmp reg, 0 相同,因为:

  • CF = OF = 0 因为 test/and 总是这样做,而对于 cmp 因为减零不能溢出或进位。
  • ZFSFPF根据结果设置(即reg):reg&reg为测试,或reg - 0对于 cmp.

(AFtest 之后未定义,但根据 cmp 的结果设置。我忽略它是因为它真的很晦涩:读取 AF 的唯一指令是ASCII 调整压缩 BCD 指令,如 AASlahf / pushf.)

您当然可以检查 reg == 0 (ZF) 以外的条件,例如通过查看 SF 测试负符号整数。但有趣的是:jl,带符号的小于条件,在 cmp 之后的某些 CPU 上比 js 更有效。它们在与零比较后是等价的,因为 OF​​=0 所以 l 条件 (SF!=OF) 等价于 SF.

每个CPU可以 TEST/JL也可以宏融合TEST/JS,甚至Core 2。但是在CMP byte [mem], 0之后,总是使用JL不JS 在符号位上分支,因为 Core 2 不能宏融合它。 (至少在 32 位模式下;Core 2 在 64 位模式下根本无法进行宏融合)。

带符号的比较条件还可以让你做类似 jle or jg 的事情,看看 ZF 和 SF!=OF。


test 的编码 比带立即数 0 的 cmp 更短,除了 cmp al, imm8 特殊情况仍然是两个字节。

即便如此,出于宏融合的原因,test 更可取(在 Core2 上有 jle 和类似的原因),并且因为根本没有立即数可以通过留下一个来帮助 uop-cache 密度另一条指令在需要更多时可以借用的插槽 space (SnB-family).


在解码器中将 test/jcc 宏融合为单个 uop

Intel 和 AMD CPUs 中的解码器可以在内部 macro-fuse testcmp 用一些条件分支指令进入单个比较和分支操作。当发生宏融合时,这为您提供了每个周期 5 条指令的最大吞吐量,而没有宏融合时为 4 条指令。 (自 Core2 以来,英特尔 CPUs。)

最近的英特尔 CPUs 可以宏融合一些指令(如 andadd/sub)以及 testcmp,但 or 不是其中之一。 AMD CPUs 只能将 testcmp 与 JCC 合并。详见, or just refer directly to Agner Fog's microarch docsCPU可以宏融合什么。 test 可以在 cmp 不能的某些情况下进行宏融合,例如js.

几乎所有简单的 ALU 运算(按位布尔值、add/sub 等)运行 都在一个周期内。它们在通过无序执行管道跟踪它们时都具有相同的“成本”。 Intel 和 AMD 使用晶体管来使快速执行单元在单个周期内达到 add/sub/whatever。是的,按位 ORAND 更简单,并且可能使用更少的功率,但仍然不能 运行 比一个时钟周期快。


or reg, reg 将另一个延迟周期 添加到依赖链中,用于后续需要读取寄存器的指令。它是导致您想要的价值的操作链中的 x |= x


您可能认为额外的寄存器写入还需要额外的 物理寄存器文件 (PRF) 条目test,但这可能是 不是这样的。 (有关 PRF 容量对乱序执行的影响的更多信息,请参阅 https://blog.stuffedcow.net/2013/05/measuring-rob-capacity/)。

test 必须在某处生成其 FLAGS 输出。至少在 Intel Sandybridge-family CPUs 上,当一条指令产生一个寄存器和一个 FLAGS 结果时,它们都存储在同一个 PRF 条目中。 (来源:我认为是英特尔的一项专利。这是来自记忆,但看起来是一个明显合理的设计。)

cmptest这样产生FLAGS结果的指令也需要一个PRF条目作为其输出。据推测,这稍微更糟:旧的物理寄存器仍然“活着”,被引用为由一些旧指令写入的体系结构寄存器的值的持有者。现在架构 EFLAGS(或更具体地说,分别重命名的 CF 和 SPAZO 标志组)指向由重命名器更新的 RAT(寄存器分配 table)中的这个新物理寄存器。当然,下一个写入 FLAGS 的指令将覆盖它,从而允许在所有读者读取并执行该 PR 后释放该 PR。这不是我在优化时考虑的事情,而且我认为在实践中并不重要。


P6 系列寄存器读取停滞:or reg,reg

的可能上升空间

P6-family CPUs(PPro / PII 到 Nehalem)有有限数量的寄存器读取端口供 issue/rename 阶段读取“冷”值(不是从输入转发的-flight 指令)来自永久寄存器文件,但最近写入的值可直接从 ROB 获得。不必要地重写寄存器可以使它再次进入转发网络,从而有助于避免寄存器读取停顿。 (参见 Agner Fog's microarch pdf)。

故意重写具有相同值的寄存器以使其保持“热”实际上可以优化 P6 上 一些 周围代码的情况。早期的 P6 家族 CPUs 根本无法进行宏融合,因此使用 and reg,reg 而不是 test 甚至不会错过这一点。但是 Core 2(在 32 位模式下)和 Nehalem(在任何模式下)可以 macro-fuse test/jcc 所以你错过了。

(and 相当于 P6 系列上的 or,但如果您的代码在 Sandybridge 系列 运行 上 CPU : 它可以宏融合 and/jcc 但不能 or/jcc. 寄存器的 dep-chain 中额外的延迟周期在 P6 上仍然是一个缺点,特别是如果涉及它的关键路径是主要瓶颈。)

P6 家族现在非常过时(Sandybridge 在 2011 年取代了它),CPUCore 2(Core、Pentium M、PIII、PII、PPro)之前的 非常 已过时并进入逆向计算领域,尤其是对于性能至关重要的任何事物。你可以在优化时忽略 P6 系列,除非你有一个特定的目标机器(例如,如果你有一台老旧的 Nehalem Xeon 机器)或者你正在为少数仍然存在的用户调整编译器的 -mtune=nehalem 设置。

如果您要在 Core 2 / Nehalem 上进行快速调整,请使用 test 除非分析显示寄存器读取停顿在特定情况下是一个大问题,并使用 and实际上修复它。

在较早的 P6 系列上,当值不是有问题的循环携带的 dep 链的一部分时,and reg,reg 可能可以作为您的默认代码生成选择,但稍后会读取。或者如果是,但还有一个特定的寄存器读取停顿,您可以使用 and reg,reg.

修复

如果您只想测试完整寄存器的低 8 位,test al,al 避免写入部分寄存器,在 P6 系列上,部分寄存器与完整 EAX/RAX 分开重命名。 or al,al 如果您稍后阅读 EAX 或 AX:P6 系列上的部分寄存器停顿会更糟。()


不幸的历史or reg,reg成语

or reg,reg 成语可能来自 8080 ORA A,正如 .

所指出的

8080's instruction set doesn't have a test instruction, so your choices for setting flags according to a value included ORA A and ANA A. (Notice that the A register destination is baked in to the mnemonic for both those instructions, and there aren't instructions to OR into different registers: it's a 1-address machine except for mov, while 大多数说明。)

8080 ORA A 是通常的首选方法,所以当人们移植他们的 asm 源代码时,大概这种习惯会延续到 8086 汇编编程中。 (或使用自动工具;8086 was intentionally designed for easy / automatic asm-source porting from 8080 code。)

这个糟糕的习语继续被初学者盲目使用,大概是由那些在当天学习并传授它的人在没有考虑无序执行的明显关键路径延迟缺点的情况下传授的。 (或者其他更微妙的问题,比如没有宏融合。)


Delphi's compiler reportedly uses or eax,eax,这在当时(在 Core 2 之前)可能是一个合理的选择,假设寄存器读取停顿比为接下来读取它的任何内容延长 dep 链更重要。 IDK,如果那是真的,或者他们只是在使用古老的成语而不考虑它。

不幸的是,当时的编译器编写者并不知道未来,因为 and eax,eax 在 Intel P6 系列上的性能与 or eax,eax 完全相同,但在其他 uarches 上性能更差,因为 and 可以在 Sandybridge 系列上进行宏融合。 (参见上面的 P6 部分)。


内存中的值:可以使用 cmp 或将其加载到注册表中。

要测试内存中的一个值,你可以cmp dword [mem], 0,但是英特尔CPUs不能宏融合标志设置指令立即数和内存操作数。如果你打算在分支的一侧使用比较后的值,你应该 mov eax, [mem] / test eax,eax 或其他东西。如果不是,两种方式都是 2 个前端微指令,但这是代码大小和后端微指令数之间的权衡。

尽管注意一些寻址模式won't micro-fuse either on SnB-family:RIP-relative + immediate 不会在解码器中微融合,或者索引寻址模式将在 uop-cache 之后取消层压。 cmp dword [rsi + rcx*4], 0 / jne[rel some_static_location].

的两种方式都会导致 3 个融合域微指令

在 i7-6700k Skylake 上(使用性能事件 uops_issued.anyuops_executed.thread 测试):

  • mov reg, [mem](或movzx)+ test reg,reg / jnz 融合域和非融合域中的 2 微指令,无论寻址模式如何,或 movzx 而不是 mov。无需微熔断;做宏融合。
  • cmp byte [rip+static_var], 0 + jne。 3 个融合,3 个未融合。 (前端和后端)。 RIP-relative + immediate 组合可防止微融合。它也不会宏融合。代码量较小但效率较低。
  • cmp byte [rsi + rdi], 0(索引地址模式)/jne 3 个融合,3 个未融合。解码器中的微型保险丝,但在 issue/rename 处未层压。不宏融合。
  • cmp byte [rdi + 16], 0 + jne 2 个融合,3 个未融合的微指令。 cmp load+ALU的微融合确实发生了,因为简单的寻址方式,但是immediate阻止了宏融合。与 load + test + jnz 一样好:更小的代码大小但 1 个额外的后端 uop。

如果寄存器中有 0(如果要比较布尔值,则为 1),则可以 cmp [mem], reg / jne 得到更少uops,低至 1 个融合域,2 个未融合。但是RIP-relative寻址模式仍然没有宏融合。

编译器倾向于使用 load + test/jcc,即使稍后不使用该值。

可以 也可以使用 test dword [mem], -1 测试内存中的值,但不要这样做。由于 test r/m16/32/64, sign-extended-imm8 不可用,因此对于任何大于字节的代码,它的代码大小都比 cmp 差。 (我认为设计思想是,如果你只想测试寄存器的低位,只需 test cl, 1 而不是 test ecx, 1,并且像 test ecx, 0xfffffff0 这样的用例很少见不值得花一个操作码。特别是因为该决定是针对 16 位代码的 8086 做出的,它只是 imm8 和 imm16 之间的区别,而不是 imm32。)

(我写了 -1 而不是 0xFFFFFFFF,所以它与 byteqword 相同。~0 是另一种写法。)

相关:

  • (微观和宏观融合)。 TODO:将测试结果移到那里(并在那里更新我的答案以修复一些与我当前结果不匹配的问题。)
  • (哪些指令可以在Sandybridge-family上宏融合)