【问题标题】:Test whether a register is zero with CMP reg,0 vs OR reg,reg?使用 CMP reg,0 vs OR reg,reg 测试寄存器是否为零?
【发布时间】:2016-02-16 17:09:08
【问题描述】:

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

cmp al, 0
je done

以及以下内容:

or al, al
jz done

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

【问题讨论】:

  • 英特尔优化手册说:使用寄存器本身的 TEST 而不是寄存器的 CMP 为零,这样就省去了对零进行编码的需要,所以这就是几乎只有大小。宏操作融合也适用于两者。快速浏览一下 Agner Fog 表可以发现,对于大多数 CPU,CMPOR 的速度相同。
  • @Jester: OR 不能与任何东西进行宏融合。较旧的 CPU (Core2) 只能使用 test 对签名比较进行宏融合,但不能对 cmp 进行宏融合。 AMD CPU 只能对 cmptest 进行宏熔断,而不是同时写入寄存器的操作。

标签: assembly optimization x86 micro-optimization


【解决方案1】:

是的,性能有所不同。

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

(更好的是ZF 已经被设置reg 的指令正确设置,因此您可以直接分支、setcc 或 cmovcc。例如,the bottom of a normal loop 通常看起来像dec ecx / @987654353 @. 大多数 x86 整数指令“根据结果设置标志”,如果输出为 0,则包括 ZF=1。)。

or reg,reg 不能将 macro-fuseJCC 放入任何现有 x86 CPU 上的单个 uop 中,并为以后读取 reg 的任何内容增加延迟,因为它将值重写到寄存器中。 cmp 的缺点通常只是代码大小。

脚注 1:可能有一个例外,但仅限于过时的 P6 系列 CPU(Intel 直至 Nehalem,2011 年被 Sandybridge 系列取代)。有关通过将相同的值重写到寄存器中来避免寄存器读取停顿的信息,请参见下文。其他微架构家族没有这样的摊位,而且or 永远比test 更胜一筹。


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

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

(AFtest 之后未定义,但根据cmp 的结果设置。我忽略它,因为它真的很模糊:读取 AF 的唯一指令是 ASCII-adjust packed-packed-BCD 指令比如AASlahf/pushf。)

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

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

签名比较条件还允许您执行 jle or jg 之类的操作,同时查看 ZF 和 SF!=OF。


testcmp 编码更短,立即为 0,在所有情况下,除了仍然是两个字节的 cmp al, imm8 特殊情况。

即便如此,由于宏融合的原因,test 更可取(在 Core2 上使用 jle 和类似的),并且因为完全没有立即数可以通过留下另一个指令可以借用的插槽来帮助 uop-cache 密度如果它需要更多空间(SnB 系列)。


将 test/jcc 宏融合到解码器中的单个 uop

Intel 和 AMD CPU 中的解码器可以在内部宏融合 testcmp 以及一些条件分支指令到单个比较和分支操作中。当发生宏融合时,这为您提供每个周期 5 条指令的最大吞吐量,而没有宏融合时为 4 条。 (适用于 Core2 以后的 Intel CPU。)

最近的 Intel CPU 可以宏融合一些指令(如 andadd/sub)以及 testcmp,但 or 不是其中之一。 AMD CPU 只能将 testcmp 与 JCC 合并。请参阅x86_64 - Assembly - loop conditions and out of order,或直接参考Agner Fog's microarch docs 了解哪个 CPU 可以宏熔断什么的详细信息。 test 可以在 cmp 不能的某些情况下进行宏融合,例如js

几乎所有简单的 ALU 运算(按位布尔运算、加/减运算等)都在一个周期内运行。它们在通过乱序执行管道跟踪它们时都有相同的“成本”。英特尔和 AMD 使用晶体管来制造快速执行单元,以便在单个周期内添加/删除/任何内容。是的,按位ORAND 更简单,并且可能使用的功率略低,但仍然不能比一个时钟周期快。


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


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

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

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


P6 系列寄存器读取停止:可能向上 or reg,reg

P6 系列 CPU(PPro / PII 到 Nehalem)具有有限数量的寄存器读取端口,用于发布/重命名阶段从永久寄存器文件中读取“冷”值(不是从运行中指令转发的) ,但最近写入的值可直接从 ROB 获得。不必要地重写寄存器可以使其再次存在于转发网络中,以帮助避免寄存器读取停顿。 (见Agner Fog's microarch pdf)。

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

(and 等效于 P6 系列上的 or,但如果您的代码曾经在 Sandybridge 系列 CPU 上运行,则不会那么糟糕:它可以宏融合 and/jcc 但不能or/jcc。寄存器的 dep 链中的额外延迟周期仍然是 P6 的劣势,特别是如果涉及它的关键路径是主要瓶颈。)

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

如果您要在 Core 2 / Nehalem 上调整某些东西以加快速度,请使用 test,除非分析表明寄存器读取停顿在特定情况下是一个大问题,并且使用 and 实际上可以解决它。

在早期的 P6 系列中,and reg,reg 可能可以作为您的默认代码生成选项,前提是该值不是有问题的循环承载的 dep 链的一部分,但稍后会读取。或者,如果是,但还有一个特定的寄存器读取停顿,您可以使用 and reg,reg 修复。

如果您只想测试完整寄存器的低 8 位,test al,al 避免写入部分寄存器,在 P6 系列上,部分寄存器与完整 EAX/RAX 分开重命名。 or al,al 更糟糕的是,如果您稍后阅读 EAX 或 AX: partial-register stall on P6-family。(Why doesn't GCC use partial registers?)


不幸or reg,reg成语的历史

or reg,reg 成语可能来自 8080 ORA A,正如 in a comment 指出的那样。

8080's instruction set 没有test 指令,因此您根据值设置标志的选择包括ORA AANA A。 (请注意,A 寄存器目标已包含在这两条指令的助记符中,并且没有对不同寄存器进行或操作的指令:除了mov,它是一个单地址机器,而大多数情况下8086 is a 2-address machine说明。)

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 可以宏融合在桑迪布里奇家族。 (参见上面的 P6 部分)。


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

要测试内存中的值,您可以cmp dword [mem], 0,但 Intel CPU 不能宏融合同时具有立即数和内存操作数的标志设置指令。如果你要在分支的一侧使用比较后的值,你应该 mov eax, [mem] / test eax,eax 或其他东西。如果不是,则任何一种方式都是 2 个前端 uop,但这是代码大小和后端 uop 数量之间的权衡。

虽然请注意某些寻址模式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 uop,无论寻址模式如何,或者movzx而不是mov。无需微熔;进行宏熔断。
  • cmp byte [rip+static_var], 0 + jne。 3个融合,3个未融合。 (前端和后端)。 RIP 相对 + 直接组合可防止微融合。它也不会宏观融合。代码量更小,但效率更低。
  • cmp byte [rsi + rdi], 0(索引地址模式)/jne 3 个已融合,3 个未融合。解码器中的微熔丝,但在问题/重命名时未层压。不会进行宏融合。
  • cmp byte [rdi + 16], 0 + jne 2 个融合的,3 个未融合的 uop。由于简单的寻址模式,确实发生了 cmp load+ALU 的微融合,但立即阻止了宏融合。与负载 + 测试 + jnz 差不多:更小的代码大小,但多了 1 个后端 uop。

如果您在寄存器中有0(如果您想比较布尔值,则为1),您可以使用cmp [mem], reg / jne 获得更少的微指令,低至1 个融合域, 2未融合。但是 RIP 相对寻址模式仍然没有宏融合。

编译器倾向于使用 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 将是另一种写法。)

相关:

【讨论】:

  • 我通常考虑微操作的数量而不是指令。一条折叠指令实际上是两个带有两个微操作的操作(算作一个微操作)。在 Haswell 上,我做了六个微操作(或操作)/时钟周期,但五个指令/周期。我不知道最大微操作/时钟周期是多少,但至少是六个。我想我的意思是操作/周期的数量更有趣。我并不是真的不同意你写的任何东西。
  • @Zboson:我通常认为是融合域微指令。我也会在相关时考虑执行端口,但如果涉及加载/存储,您通常会受到前端/管道宽度(4 微指令/时钟)的限制,而不是执行资源。 (当然假设您不受 dep 链或缓存未命中的限制。)我只是指出指令/时钟作为解释 为什么 发生宏融合很重要的一种方式。
  • 我认为OR AL,AL的起源可以追溯到8080上的ORA A。由于MSDOS API中最古老的部分是模仿CP/M的部分,以便于移植,我可以想象一下,许多早期的 DOS 代码都受到了 8080 上开始存在的代码的严重影响。
  • @MikeB:uops.info 是最好的电流源,具有可靠的自动化测试。对于较旧的 CPU,Agner Fog 的指令表通常非常好,而且几乎没有拼写错误...agner.org/optimize。对于分析指令序列,有 Intel 的 IACA(已报废)What is IACA and how do I use it?,以及开源 LLVM-MCA llvm.org/docs/CommandGuide/llvm-mca.html
  • @ecm:感谢校对! IIRC,我的意思是“即使以后不使用该值”。讨厌的多动症,我反复编辑了这个答案的不同部分,而不是在一个地方完成一个想法:P
【解决方案2】:

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

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

cmp al,0 的主要问题是它稍大,这可能意味着指令获取速度较慢/缓存压力更大,并且(如果是循环)可能意味着代码不再适合某些 CPU 的“循环缓冲区” .

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

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

【讨论】:

  • AL 中的值来自 BIOS 中断,因此这不符合“适当设置标志”的条件...iret 无论如何都会恢复标志。我还想到了一个使用lodsb 并检查空终止符的print 子例程,lodsb 是否根据 AL 中的内容更改标志?
  • @AnonymousShadow 在这种情况下,您的比较指令的性能微不足道,您不必担心。 BIOS 中断至少需要数百个周期,对于慢速 I/O 操作,最多需要数十亿个周期。
  • @RossRidge 使用带有大字符串的 LODSB 怎么样?无论如何,大小都会有所不同,不妨使用它。
  • @AnonymousShadow:如果优化代码大小,请使用lodsb。否则,mov al, [esi] / inc esi 在 Intel CPU(例如 Haswell)上仅解码为 2 微指令而不是 3,因此它可能运行得更快。根据您的循环,您可能能够使用更复杂的寻址模式(更小的代码大小,但 2 寄存器寻址模式不能在英特尔 SnB 系列上进行微熔断)来避免指针增量。请参阅我的回答,了解为什么 test 出于同样的原因更好(由于与分支的宏融合,微指令更少)。如果您使用 setcc 来使用标志,而不是分支,那么它就不那么重要了。
  • @Brendan test al,alcmp al,0 都占用 2 个字节。只有当您开始使用另一个寄存器时,大小才会有所不同。
猜你喜欢
  • 2010-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-15
相关资源
最近更新 更多