使用以下代码有执行速度差异吗:
cmp al, 0
je done
以及以下内容:
or al, al
jz done
我知道 JE 和 JZ 指令是相同的,同时使用 OR 可以减少一个字节的大小。然而,我也关心代码速度。逻辑运算符似乎比 SUB 或 CMP 更快,但我只是想确认一下。这可能是大小和速度之间的权衡,或者是双赢(当然,代码将更加难懂)。
使用以下代码有执行速度差异吗:
cmp al, 0
je done
以及以下内容:
or al, al
jz done
我知道 JE 和 JZ 指令是相同的,同时使用 OR 可以减少一个字节的大小。然而,我也关心代码速度。逻辑运算符似乎比 SUB 或 CMP 更快,但我只是想确认一下。这可能是大小和速度之间的权衡,或者是双赢(当然,代码将更加难懂)。
是的,性能上是有区别的。
比较一个寄存器是否为零的最佳选择是test reg, reg
。它可以像cmp reg, 0
一样设置FLAGS,而且速度至少和其他方式一样快1,并且代码大小更小。
(当ZF
已经由设置reg
的指令适当设置时,最好直接跳转、setcc或cmovcc。例如,普通循环的底部通常看起来像dec ecx
/jnz .loop_top
。大多数x86整数指令“根据结果设置标志”,包括ZF=1如果输出是0
。)
或 reg,reg
在任何现有的x86 CPU上都无法与宏融合和JCC合并为单个uop,并且会增加后续读取reg
的任何内容的延迟,因为它将该值重新写入寄存器。 cmp
的缺点通常只是代码大小。
注1:可能有一些例外,但仅限于过时的P6系列CPU(Intel Nehalem以前,2011年被Sandybridge系列替代)。 请参见下面有关通过将相同的值重写到寄存器中以避免寄存器读取停顿的方法。 其他微架构系列没有这样的停顿,而且使用test
总是更优。
FLAGS 的结果与 test reg,reg
/ and reg,reg
/ or reg,reg
相同,
除了 AF 标志位之外,在所有情况下都等同于 cmp reg, 0
,因为:
CF = OF = 0
,因为 test
/and
总是这样做,对于 cmp
来说,减去零不会溢出或进位。ZF
、SF
、PF
根据结果(即 reg
)设置:test
的结果为 reg®
,cmp
的结果为 reg - 0
。test
后,AF
未定义,但根据cmp
的结果进行设置。我忽略它,因为它非常模糊:唯一读取AF的指令是ASCII调整打包BCD指令,例如AAS
和lahf
/pushf
。reg == 0
(ZF)之外的其他条件,例如通过查看SF来测试负有符号整数。但有趣的事实是,在某些CPU上,经过cmp
后,有符号小于条件jl
比js
更有效率。它们在与零比较后是等效的,因为OF=0,所以l
条件(SF!=OF
)等效于SF
。CMP byte [mem], 0
之后,始终使用JL而不是JS来分支符号位,因为Core 2无法宏熔丝。 (至少在32位模式下;Core 2在64位模式下根本无法宏熔丝)。jle
或jg
的操作,同时查看ZF以及SF!= OF。
test
的编码比带有立即数0的cmp
更短,除了cmp al, imm8
这种特殊情况仍然是两个字节。
即使在这种情况下,由于宏融合的原因(在Core2上与jle
等一起使用),以及完全没有立即数可能会通过留出另一个指令可以借用的空间来帮助uop缓存密度(SnB-family),所以test
更可取。
Intel和AMD CPU中的解码器可以将一些条件分支指令与test
和cmp
内部进行宏融合,从而形成单个比较和分支操作。当发生宏融合时,最大吞吐量为每个周期5条指令,而没有宏融合则为4条指令。(对于自Core2以来的Intel CPU。)
and
和add
/sub
)以及test
和cmp
作为宏融合,但是or
不是其中之一。AMD CPU只能将test
和cmp
与JCC合并。详细信息请参见x86_64 - Assembly - loop conditions and out of order或直接参考Agner Fog的微架构文档,了解哪个CPU可以将哪个指令宏融合。在某些情况下,test
可以宏融合而cmp
则不能,例如使用js
。OR
或AND
更简单,可能使用的功率略低,但仍无法比一个时钟周期更快地运行。
or reg, reg
增加了另一个时钟周期的延迟,对于需要读取寄存器的后续指令的依赖链来说。它是操作链中的 x |= x
,这些操作导致你想要的值。
test
相比,但这可能不是情况。(有关PRF容量对乱序执行的影响,请参见https://blog.stuffedcow.net/2013/05/measuring-rob-capacity/。)
test
必须在某个地方产生其FLAGS输出。至少在英特尔Sandybridge系列CPU上,当一条指令产生一个寄存器和一个FLAGS结果时,它们都存储在同一个PRF条目中。(来源:我认为是英特尔专利。这是根据记忆而言,但似乎是一个明显合理的设计。)
像cmp
或test
这样仅产生FLAGS结果的指令还需要一个PRF条目来存储其输出。大概这会稍微糟糕一些:旧的物理寄存器仍然存在,被引用为某个早期指令写入的体系结构寄存器值的持有者。现在,体系结构EFLAGS(更具体地说,是CF和SPAZO标志组)在由重命名器更新的RAT(寄存器分配表)中指向这个新的物理寄存器。当然,下一个写入FLAGS的指令将覆盖它,从而允许该PR在所有读取它并执行的读者完成后被释放。这不是我在优化时考虑的问题,我认为在实践中也不太重要。
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(在任何模式下)可以宏融合测试/ jcc,因此您会错过这一点。
and
与or
用于此目的是等效的,但如果您的代码运行在Sandybridge系列CPU上,则后者不太好:它可以将and
/jcc
进行宏融合,但不能将or
/jcc
进行宏融合。寄存器中的依赖链的额外周期仍然是P6上的一个缺点,特别是如果其中涉及的关键路径是主要瓶颈。
P6系列已经非常过时(Sandybridge在2011年替代了它),而Core 2之前的CPU(Core、Pentium M、PIII、PII、PPro)则非常过时,甚至已经步入复古计算领域,尤其是对于任何性能至关重要的事情。优化时,除非您有特定的目标机器(例如,如果您有一个陈旧的Nehalem Xeon机器)或者您正在调整编译器的-mtune = nehalem
设置以适应少数仍然存在的用户,否则可以忽略P6系列。
如果您要调整某些东西以在Core 2 / Nehalem上运行更快,请使用test
,除非分析显示在特定情况下,寄存器读取停顿是一个大问题,并且使用and
实际上可以解决它。
and reg,reg
可能是您默认的代码生成选择。或者如果它确实是一个问题,但也有一个特定的寄存器读取停顿,您可以使用 and reg,reg
修复它。test al,al
避免了写入部分寄存器,在P6系列中,这与完整的EAX / RAX分别重命名。如果您稍后读取EAX或AX,则 or al,al
更糟糕:在P6系列上产生部分寄存器停顿。(为什么GCC不使用部分寄存器?)
or reg,reg
习语的历史or reg,reg
习语可能来自于8080的ORA A
,正如在评论中指出的。
8080的指令集没有test
指令,因此您可以选择根据值设置标志的选项包括ORA A
和ANA A
。(请注意,对于这两个指令,A
寄存器目的地已经内置在助记符中,并且没有将OR运算符写入不同寄存器的指令:它是一个1地址机器,除了mov
外,而8086对于大多数指令来说是一个2地址机器。)
8080 ORA A
是通常的做法,因此可以推断人们在移植汇编源代码时将其带入了 8086 汇编程序设计中。 (或使用自动工具; 8086 was intentionally designed for easy / automatic asm-source porting from 8080 code。)
这种不良习惯继续被初学者盲目使用,可能是由过去学习并毫无思考地传授下来的人所教。这种做法明显会对乱序执行的关键路径延迟产生负面影响(或其他更微妙的问题,如无宏融合)。
Delphi的编译器据报道使用了or eax,eax
,这可能是一个合理的选择(在Core 2之前),假设寄存器读取停顿比延长下一个读取的dep链更重要。我不知道这是否正确,或者他们只是在不加思考地使用古老的习语。
不幸的是,当时的编译器编写者并不知道未来,因为and eax,eax
在Intel P6系列上执行与or eax,eax
完全相同,但在其他uarch上不那么糟糕,因为and
可以在Sandybridge系列上进行宏融合。(请参见上面的P6部分)。
cmp
或将其加载到寄存器中。要测试内存中的值,您可以使用cmp dword [mem], 0
,但是Intel CPU无法宏观融合同时具有立即和内存操作数的设置标志指令。如果您在分支的一侧使用比较后的值,则应该使用mov eax,[mem]
/ test eax,eax
或类似的东西。否则,两种方式都是2个前端uop,但这是代码大小和后端uop计数之间的权衡。
尽管请注意,某些寻址模式在SnB系列上也不会微聚:RIP相对+立即数在解码器中不会微聚,或者索引寻址模式将在uop缓存之后取消层压。这两种方式都导致cmp dword [rsi + rcx * 4],0
/ jne
或[rel some_static_location]
的3个融合域uop。
在i7-6700k Skylake上(使用性能事件uops_issued.any
和uops_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进行了微型融合,但是立即数防止了宏融合。与load + test + jnz一样好:代码尺寸更小但需要1个额外的后端uop。0
(或者一个1
用于比较布尔值),您可以使用cmp [mem], reg
/jne
来进行比较,这样可以减少uops的数量,甚至可以降低到1个融合域和2个未融合域。但是,RIP相对寻址模式仍然无法宏观融合。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,这样就可以与byte
或qword
相同。~0
是另一种写法。)
相关:
OR AL,AL
的起源可以追溯到8080上的ORA A
。由于MSDOS API的最古老部分是模仿CP/M而设计的,以便于移植,我可以想象早期的DOS代码受到了在8080上开始存在的代码的严重影响。 - fvu这取决于确切的代码序列、具体的CPU以及其他因素。
or al,al
的主要问题在于它“修改”了 EAX
,这意味着后续使用 EAX
的指令可能会停顿,直到此指令完成。请注意,条件分支(jz
)也取决于该指令,但是CPU制造商做了大量工作(分支预测和推测执行)来缓解这个问题。同时请注意,在理论上,CPU制造商可以设计一种CPU,识别出在这种特定情况下 EAX
没有被改变,但是有数百个这样的特殊情况,识别其中大多数的好处太小。
cmp al,0
的主要问题在于它稍微大一些,这可能意味着较慢的指令获取/更高的缓存压力,并且(如果它是一个循环)可能意味着代码不再适合某些CPU的“循环缓冲区”。
正如Jester在评论中指出的那样,test al,al
避免了这两个问题- 它比 cmp al,0
更小,并且不会修改 EAX
。
当然(根据具体的序列),AL
中的值必须来自某处,如果它来自一个适当设置标志的指令,可能可以修改代码以避免稍后再使用另一个指令来设置标志。
iret
无论如何都会恢复标志。我还考虑了一个使用 lodsb
的 print
子程序,并检查空终止符,那么 lodsb
是否会根据 AL 中的内容改变标志呢? - sadljkfhalskdjfhlodsb
。否则,在英特尔CPU(例如Haswell)上,mov al,[esi] / inc esi
解码仅需要2个uop而不是3个,因此它可能运行得更快。根据您的循环,您可以使用更复杂的寻址模式来避免指针增量(代码大小较小,但2个寄存器寻址模式无法在英特尔SnB系列上微融合)。请参阅我的答案,了解为什么“test”出于相同的原因更好(由于与分支的宏融合,uops更少)。如果您正在使用setcc
来消耗标志,而不是分支,则重要性较小。 - Peter Cordestest al,al
和 cmp al,0
占用2个字节。只有当你开始使用另一个寄存器时,它们的大小才会有所不同。 - Sep Roland
OR
无法与任何东西宏合并。旧的CPU(Core2)只能将有符号比较与test
宏合并,而不能与cmp
宏合并。AMD CPU只能宏合并cmp
和test
,而不能宏合并还写入寄存器的操作。 - Peter Cordes