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

sad*_*jfh 13 optimization x86 assembly micro-optimization

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

cmp al, 0
je done
Run Code Online (Sandbox Code Playgroud)

以下内容:

or al, al
jz done
Run Code Online (Sandbox Code Playgroud)

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

Pet*_*des 23

是的,性能有所不同.

在现代x86上比较寄存器与零的最佳选择test reg, reg(如果ZF尚未通过设置的指令进行适当设置reg).这就像AND reg,reg但没有写目的地.

or reg,reg不能进行宏融合,为以后读取它的任何东西增加延迟,并且它需要一个新的物理寄存器来保存结果.(因此它会使用寄存器重命名资源,test而不会限制CPU的无序指令窗口).(重写dst可能是英特尔P6系列的胜利,但见下文.)


//的标志结果与所有情况相同(AF除外):test reg,regand reg,regor reg,regcmp reg, 0

  • CF = OF = 0因为test/ and总是这样做,cmp因为减零不能溢出或携带.
  • ZF,SF,PF根据结果(即设置reg):reg&reg用于测试,或reg - 0用于CMP.因此,您可以通过查看SF来测试负有符号整数或无符号高位设置.

    或者jl,因为OF = 0所以lcondition(SF!=OF)等价于SF.每个可以 TEST/JL进行宏熔丝的CPU也可以 TEST/JS进行微距融合,甚至是Core2.但之后CMP byte [mem],0,总是使用JL而不是JS来分支符号位.

(之后AF是未定义的test,但根据结果设置cmp.我忽略了它,因为它真的很模糊:AF的唯一消费者是ASCII调整打包BCD指令,如AASlahf/ pushf.)


test编码cmp使用立即0 更短,在所有情况下除了cmp al, imm8仍然是两个字节的特殊情况.即使这样,test最好是出于宏观融合的原因(jle在Core2上也是如此),并且因为没有立即可能通过留下另一条指令可以借用的插槽来帮助uop-cache密度,如果它需要更多空间(SnB-family) ).


英特尔和AMD CPU中的解码器可以在内部进行宏熔丝 test,cmp并将一些条件分支指令转换为单个比较和分支操作.当宏观融合发生时,这为每个周期提供了5个指令的最大吞吐量,而没有宏融合的情况下为4个.(对于Core2以来的Intel CPU.)

最近英特尔CPU可以宏观保险丝一些指令(像andadd/ sub),以及testcmp,而or不是其中之一.AMD的CPU只能合并testcmp与JCC.请参阅x86_64 - 汇编 - 循环条件和乱序,或者直接参考Agner Fog的微架文档,了解哪些CPU可以宏观融合的内容. test在某些情况下,cmp如果不能,可以进行宏观融合js.

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


此外,正如Brendan指出的那样,为依赖于需要读取寄存器的指令or reg, reg添加了依赖链的另一个延迟周期.

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

据报道or eax,eax,Delphi的编译器使用了当时的合理选择,假设寄存器读取停顿比延长下一个读取链的dep链更重要.

不幸的是,当时的编译器编写者并不知道未来,因为在英特尔P6系列上and eax,eax完全等效or eax,eax,但在其他搜索上却不那么糟糕,因为and可以在Sandybridge系列上进行宏观融合.

对于Core2/Nehalem(最后2个P6系列的搜索),test可以进行宏保险但and不能,因此(与Pentium II/III/M不同)它是宏融合和可能减少寄存器读取停顿之间的权衡.该寄存器读失速避免不还是来了额外的延迟的成本,如果该值被测试后读取,所以test可以比一个更好的选择and,即使之前在某些情况下cmovsetcc,而不是jcc,或无CPU的宏融合.

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

IDK来自哪个or reg,reg成语,除了它可能更短.或者它可能是故意用于P6 CPU在使用它之前故意重写寄存器.当时的编码人员无法预测它的效率会低于and此目的.但很明显,我们不应该在使用它testand在新的代码.(当它出现jcc在Sandybridge家族之前时,只会有一点不同,但是忘记它会更简单or reg,reg.)


要测试内存中的值,可以cmp dword [mem], 0,但Intel CPU不能对具有立即数和内存操作数的标志设置指令进行宏熔合.如果你打算在分支的一侧使用比较后的值,你可能应该mov eax, [mem]/ test eax,eax或者什么.如果不是(例如测试一个布尔值),cmp使用内存操作数就可以了.

虽然注意到一些寻址模式不会在SnB系列上微熔合:RIP相对+立即不会在解码器中微熔合,或者索引寻址模式将解除封装.无论哪种方式导致3个融合域uops为cmp dword [rsi + rcx*4], 0/ jne[rel some_static_location].

可以使用test dword [mem], -1但不是在内存中测试值.由于test r/m16/32/64, sign-extended-imm8不可用,它的代码大小比cmp大于字节的任何东西都要差.(我认为设计理念是,如果你只想测试一个寄存器的低位,test cl, 1而不是test ecx, 1,并使用类似test ecx, 0xfffffff0的情况,这是不值得花费操作码.特别是因为这个决定是为了8086与16位代码,其中只有imm8和imm16之间的区别,而不是imm32.)

我写了-1而不是0xFFFFFFFF所以它与byteor 相同qword. ~0将是另一种写它的方式.


Bre*_*dan 11

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

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

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

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

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

  • @Brendan`test al,al`和`cmp al,0`都占用2个字节.只有当您开始使用尺寸不同的其他寄存器时. (2认同)

归档时间:

查看次数:

3755 次

最近记录:

7 年,7 月 前