(Even更好的情况是ZF已经被设置reg的指令适当地设置了,所以你可以直接分支,setcc或cmovcc。例如,the bottom of a normal loop通常看起来像dec ecx/jnz .loop_top。大多数x86整数指令“根据结果设置标志”,包括ZF=1,如果输出是0。 在任何现有的x86 CPU上,or reg,reg不能与JCC宏融合成单个uop,并且增加了以后读取reg的任何东西的延迟,因为它将值重写到寄存器中。cmp的缺点通常只是代码大小。 test reg,reg/and reg,reg/or reg,reg的FLAGS结果为
2条答案
按热度按时间mm9b1k5b1#
是的,性能有区别。
**比较寄存器与0的最佳选择是
test reg, reg
。它设置FLAGS的方式与cmp reg,0
相同,**并且至少与任何其他方式一样快1,代码大小更小。(Even更好的情况是
ZF
已经被设置reg
的指令适当地设置了,所以你可以直接分支,setcc或cmovcc。例如,the bottom of a normal loop通常看起来像dec ecx
/jnz .loop_top
。大多数x86整数指令“根据结果设置标志”,包括ZF=1,如果输出是0
。在任何现有的x86 CPU上,
or reg,reg
不能与JCC宏融合成单个uop,并且增加了以后读取reg
的任何东西的延迟,因为它将值重写到寄存器中。cmp
的缺点通常只是代码大小。test reg,reg
/and reg,reg
/or reg,reg
的FLAGS结果为在所有情况下都与
cmp reg, 0
相同(AF除外),因为:CF = OF = 0
,因为test
/and
总是这样做,而对于cmp
,因为减去零不能溢出或进位。ZF
、SF
、PF
根据结果设置(即reg
):reg®
用于测试,或reg - 0
用于cmp。(
AF
在test
之后未定义,但根据cmp
的结果设置。我忽略了它,因为它真的很模糊:唯一读取AF的指令是ASCII调整打包BCD指令,如AAS
和lahf
/pushf
。)当然,你也可以检查
reg == 0
(ZF)以外的条件,例如,通过查看SF来测试负符号整数。但有趣的事实是:jl
,有符号小于条件,在cmp
之后的某些CPU上,l
比js
更高效。它们在与零比较后是等效的,因为OF=0,所以l
条件(SF!=OF
)等于SF
。每个可以宏融合TEST/JL的CPU也可以宏融合TEST/JS,即使是Core 2。但是在
CMP byte [mem], 0
之后,总是使用JL而不是JS在符号位上进行分支,因为Core 2不能宏融合。(至少在32位模式下; Core 2在64位模式下根本不能宏融合)。符号比较条件还允许您执行
jle
orjg
之类的操作,查看ZF以及SF!=OF。**
test
比立即0的cmp
更短,在所有情况下,除了cmp al, imm8
特殊情况,它仍然是两个字节。即使如此,
test
也是出于宏融合的原因(与jle
以及Core 2上的类似情况),并且因为根本没有立即数可以通过留下一个插槽来帮助uop缓存密度,如果另一个指令需要更多空间(SnB系列),则可以借用该插槽。在解码器中将test/jcc宏融合为单个uop
Intel和AMD CPU中的解码器可以在内部macro-fuse
test
和cmp
,并将一些条件分支指令合并为一个比较和分支操作。这使您在宏融合发生时每个周期的最大吞吐量为5条指令,而没有宏融合时为4条。(适用于Core 2之后的Intel CPU)最近的英特尔CPU可以宏融合一些指令(如
and
和add
/sub
)以及test
和cmp
,但or
不是其中之一。AMD CPU只能用JCC合并test
和cmp
。参见x86_64 - Assembly - loop conditions and out of order,或者直接参考Agner Fog's microarch docs以了解哪些CPU可以宏融合什么。在某些情况下,test
可以宏融合,而cmp
不能,例如与js
。几乎所有简单的ALU操作(按位布尔、加/减等)在单个周期内运行。它们在通过无序执行管道跟踪它们时都有相同的“成本”。Intel和AMD花费晶体管来制造快速执行单元,以便在单个周期内执行加/减/什么。是的,按位
OR
或AND
更简单,可能使用的功率略低,但是仍然不能运行得比一个时钟周期快。or reg, reg
为后续需要读取寄存器的指令的依赖链增加了另一个延迟周期。它是导致您稍后想要的值的操作链中的x |= x
。您可能认为额外的寄存器写入也需要额外的物理寄存器文件(PRF)条目,而不是
test
,但情况可能并非如此。(有关PRF容量对无序exec的影响的更多信息,请参见https://blog.stuffedcow.net/2013/05/measuring-rob-capacity/)。test
必须在某个地方产生它的FLAGS输出。至少在Intel Sandybridge系列CPU上,当一条指令产生一个寄存器和一个FLAGS结果时,它们都被存储在同一个PRF条目中。(来源:我想是Intel的一项专利。这是来自内存,但似乎是一个明显合理的设计,因为大多数x86 ALU指令都写FLAGS。)像
cmp
或test
这样的指令,* 只 * 产生FLAGS结果,也需要一个PRF条目来输出。大概这稍微 * 更糟糕 *:旧的物理寄存器仍然是“活的”,作为由一些旧指令写入的架构寄存器的值的保持器引用。现在是架构EFLAGS(实际上separately-renamed CF和SPAZO标志组)指向RAT中的这个新物理寄存器(寄存器分配表)被重命名器更新。当然,下一个FLAGS写入指令将覆盖它,允许PR在所有的读者都读过并执行之后被释放。这不是我在优化时考虑的问题,我认为在实践中也不重要。Footnote 1:P6系列寄存器读取暂停:可能向上扩展到
or reg,reg
这 * 仅 * 适用于过时的P6系列CPU(Intel到Nehalem,在2011年被Sandybridge系列取代)。在其他CPU上,用
or same,same
重写寄存器本身而不是用test same,same
阅读它没有任何好处。P6系列CPU(PPro / PII到Nehalem)具有有限数量的寄存器读取端口,用于发布/重命名阶段读取“冷”值从永久寄存器文件中读取(不是从运行中指令转发),但最近写入的值可以直接从ROB中获得。不必要地重写寄存器可以使其再次在转发网络中活动,以帮助避免寄存器读取停顿。(参见Agner Fog's microarch pdf)。
在P6上,用相同的值重写一个寄存器来保持它的“热”,实际上是对周围代码的 * 某些 * 情况的优化。早期的P6系列CPU根本不能进行宏融合,因此,使用
and reg,reg
而不是test
,您甚至不会错过这一点。(在32位模式下)和Nehalem(在任何模式下)can macro-fuse test/jcc,所以你错过了这一点。(在P6系列上,
and
等效于or
,但如果您的代码在Sandybridge系列CPU上运行,则会更好:它可以宏融合and
/jcc
,但不能宏融合or
/jcc
。寄存器的dep-chain中的额外延迟周期仍然是P6的缺点,特别是如果涉及它的关键路径是主要瓶颈的话。P6系列现在已经过时了(Sandybridge在2011年取代了它),以及Core 2之前的CPU(核心,奔腾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系列上的部分寄存器停顿。(Why doesn't GCC use partial registers?)不幸的
or reg,reg
习惯用法的历史or reg,reg
习惯用法可能来自8080ORA A
,正如评论中指出的那样。8080's instruction set没有
test
指令,因此您可以根据ORA A
和ANA A
中的值设置标志。(请注意,A
寄存器目的地被烘焙到这两个指令的助记符中,并且没有指令要OR到不同的寄存器中:除了mov
之外,它是一个单地址机器,而8086 is a 2-address machine用于大多数指令。)8080
ORA A
是最常用的方法,所以当人们移植他们的asm源代码(或者使用自动化工具; 8086 was intentionally designed for easy / automatic asm-source porting from 8080 code)时,这个习惯大概也被带到了8086汇编编程中。这个糟糕的习惯用法仍然被初学者盲目地使用,大概是那些在过去学习过它的人教的,他们没有考虑乱序执行的明显关键路径延迟(或者其他更微妙的问题,比如没有宏融合)。
Delphi's compiler reportedly uses
or eax,eax
,这在当时(在Core 2之前)可能是一个合理的选择,假设寄存器读取暂停比延长dep链更重要,无论接下来读什么。IDK如果这是真的,或者他们只是在使用古老的习惯用法而没有考虑它。不幸的是,当时的编译器作者并不知道未来,因为
and eax,eax
在Intel P6系列上的性能与or eax,eax
完全相同,但在其他uarch上没有那么糟糕,因为and
可以在Sandybridge系列上进行宏融合。内存中的值:可能使用
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- 6700 k Skylake上(使用perf事件
uops_issued.any
和uops_executed.thread
进行测试):mov reg, [mem]
(或movzx
)+test reg,reg / jnz
2 uops(在融合和未融合域中),无论寻址模式如何,或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个fused,3个unfused uop。由于简单的寻址模式,cmp load+ALU的微融合确实发生了,但立即阻止了宏融合。与load + test + jnz一样好:代码大小更小,但多了1个后端uop。如果你在一个寄存器中有一个
0
(或者如果你想比较bool,则是一个1
),你可以为更少的uop使用cmp [mem], reg
/jne
,低至1个fused-domain,2个unfused。编译器倾向于使用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做出的,其中只有imm 8和imm 16之间的区别,而不是imm 32。)(我写了-1而不是0xFFFFFFFF,所以它与
byte
或qword
相同。~0
将是另一种写法。相关:
inb24sb22#
这取决于确切的代码序列,它是哪个特定的CPU,以及其他因素。
or al, al,
的主要问题是它“修改”了EAX
,这意味着以某种方式使用EAX
的后续指令可能会停止,直到该指令完成。* 注意,条件分支(jz
)也取决于指令,但是CPU制造商做了很多工作(分支预测和推测执行)还要注意的是,理论上CPU制造商可以设计一种CPU,它可以识别EAX
在这种特定情况下没有改变,但是,这样的特殊情况有数百种,承认其中大多数的好处太少了。cmp al,0
的主要问题是它稍大,这可能意味着更慢的指令获取/更多的缓存压力,并且(如果它是一个循环)可能意味着代码不再适合某些CPU的“循环缓冲区”。正如Jester在评论中指出的那样;
test al,al
避免了这两个问题-它比cmp al,0
小,并且不会修改EAX
。当然(取决于特定的序列)
AL
中的值必须来自某个地方,如果它来自一条适当设置标志的指令,那么可以修改代码以避免稍后再次使用另一条指令设置标志。