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

时间:2015-11-15 15:08:34

标签: assembly optimization x86 micro-optimization

使用以下代码是否存在任何执行速度差异:

cmp al, 0
je done

以及以下内容:

or al, al
jz done

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

2 个答案:

答案 0 :(得分:21)

,性能存在差异。

在现代x86上比较一个名为的寄存器的最佳选择是test reg, reg(如果ZF尚未通过设置{{reg的指令进行适当设置1}})。它就像AND reg,reg但没有写目的地。

or reg,reg无法进行宏融合,为以后读取它的任何内容增加延迟,并且需要一个新的物理寄存器来保存结果。 (因此,它会使用注册表重命名资源,其中test不会,limiting the CPU's out-of-order instruction window)。 (重写dst可能是英特尔P6系列的胜利,但见下文。)

flag / and reg,reg / or reg,reg的{​​{3}}结果在所有情况下均与test reg,reg相同(AF除外):

  • CF = OF = 0因为test / and总是这样做,而cmp因为减去零不会溢出或携带。
  • ZFSFPF根据结果设置(即reg):reg&reg进行测试,或{{1}对于cmp。因此,您可以通过查看SF来测试负有符号整数或无符号高位设置。

    reg - 0,因为OF = 0,因此jl条件(l)相当于SF!=OF。每个可以cmp reg, 0 TEST / JL的CPU也可以对TEST / JS进行宏融合,甚至是Core2。但是在SF之后,总是使用JL而不是JS来分支符号位。

CMP byte [mem],0AF未定义,但根据test的结果设置。我忽略了它,因为它真的很模糊:唯一的消费者for AF是ASCII调整的压缩BCD指令,如macro-fusecmp / lahf。)

pushf特殊情况(仍为两个字节)外,

test编码的时间比cmp短,而且速度为0。即便如此,cmp al, imm8因宏观融合的原因(在Core2上使用test和类似内容)更受欢迎,并且因为没有立即可能通过留下另一条指令可以使用的插槽来帮助uop-cache密度如果需要更多空间(SnB家族),请借用。

Intel和AMD CPU中的解码器可以在内部宏融合 jletest,并将一些条件分支指令转换为单个比较和分支操作。当宏观融合发生时,这使得每个周期的最大吞吐量为5个指令,而没有宏融合的情况下为4个。 (对于Core2以来的Intel CPU。)

最近的英特尔CPU可以对某些指令(如cmpand / add)以及subtest进行宏观融合,但{{{ 1}}不是其中之一。 AMD CPU只能将cmpor与JCC合并。请参阅AAS,或直接参考x86_64 - Assembly - loop conditions and out of order,了解哪些CPU可以将哪些内容进行宏观融合。在test无法解决的情况下,cmp可以进行宏观融合。与test

几乎所有简单的ALU操作(按位布尔,添加/子等)都在一个循环中运行。他们都有相同的"成本"通过无序执行管道跟踪它们。英特尔和AMD花费晶体管来制作快速执行单元,以便在一个周期内添加/子/任何内容。是的,按位cmpjs更简单,可能耗电更少,但仍然可以比一个时钟周期更快地运行。

另外,正如Brendan指出的那样, OR为依赖关系链添加了另一个延迟周期,以便跟踪需要读取寄存器的指令。

但是,P6系列CPU上的(PPro / PII到Nehalem),写入目标寄存器实际上可能是一个优势。用于从永久寄存器文件读取的发出/重命名阶段的寄存器读取端口数量有限,但最近写入的值可直接从ROB获得。不必要地重写寄存器可以使其再次存在于转发网络中以帮助避免寄存器读取停顿。 (见Agner Fog's microarch docs

Agner Fog's microarch pdf,这在当时是一个合理的选择,假设寄存器读取停顿对于延长下一个读取它的dep链来说更重要。

不幸的是,当时的编译器编写者并不知道未来,因为AND与英特尔P6系列上的or reg, reg完全等效,但在其他搜索上却不那么糟糕,因为{ {1}}可以在Sandybridge家族中融合。

对于Core2 / Nehalem(最后2个P6家族的搜索),and eax,eax可以进行宏观融合但or eax,eax不能,因此(与Pentium II / III / M不同) #39;在宏观融合和可能减少寄存器读取停顿之间进行权衡。如果在测试后读取值,则寄存器读取停止避免仍然以额外延迟为代价,因此and在某些情况下甚至可能在test之前更好。 {1}}或and,而不是test,或者没有宏融合的CPU。

如果您要在多个搜索范围内快速调整某些内容,请使用and,除非分析显示注册读取停顿在Core2 / Nehalem的特定情况下是一个大问题,并使用{{1实际上修复了它。

IDK,cmov成语来自哪里,除非输入的内容更短。或者它可能是故意用于P6 CPU在使用它之前故意重写寄存器。当时的编码人员无法预测,为此目的,最终效率低于setcc。但显然我们不应该在新代码中使用jcctest。 (只有在Sandybridge家庭的and之前,才会有区别,但忘记or reg,reg更简单。)

要测试内存中的值,它对and来说没问题,但是英特尔CPU不能立即使用宏保险丝标志设置指令和内存操作数。如果您要在分支的一侧使用比较后的值,则应该test / and或其他内容。如果不是(例如测试一个布尔值),带有内存操作数的jcc就可以了。

虽然注意到某些寻址模式Delphi's compiler reportedly uses or eax,eax:RIP相对+立即赢得了解码器中的微熔丝,或者索引寻址模式将取消层压。无论哪种方式导致or reg,reg / cmp dword [mem], 0mov eax, [mem]的3个融合域uops。

还可以<{1}}在内存中测试一个值,但不要。由于test eax,eax不可用,因此对于大于字节的任何内容,代码大小都会比cmp更差。 (我认为设计理念是,如果你只想测试寄存器的低位,只需要cmp dword [rsi + rcx*4], 0而不是jne,而像[rel some_static_location]这样的用例很少见到它不值得花一个操作码。特别是因为那个决定是用80位的16位代码做出来的,只有imm8和imm16之间的区别,而不是imm32。)

我写了-1而不是0xFFFFFFFF所以它与test dword [mem], -1test r/m16/32/64, sign-extended-imm8相同。 cmp是另一种写作方式。

答案 1 :(得分:10)

这取决于确切的代码序列,特定的CPU,以及其他因素。

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

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

正如杰斯特在评论中指出的那样; test al,al可以避免这两个问题 - 它比cmp al,0小,并且不会修改EAX

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