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

epfja78i  于 11个月前  发布在  其他
关注(0)|答案(2)|浏览(120)

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

cmp al, 0
je done

字符串
以及以下内容:

or al, al
jz done


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

mm9b1k5b

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,regFLAGS结果为

在所有情况下都与cmp reg, 0相同(AF除外),因为

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

AFtest之后未定义,但根据cmp的结果设置。我忽略了它,因为它真的很模糊:唯一读取AF的指令是ASCII调整打包BCD指令,如AASlahf/pushf。)
当然,你也可以检查reg == 0(ZF)以外的条件,例如,通过查看SF来测试负符号整数。但有趣的事实是:jl,有符号小于条件,在cmp之后的某些CPU上,ljs更高效。它们在与零比较后是等效的,因为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 or jg之类的操作,查看ZF以及SF!=OF。

**test比立即0的cmp更短,在所有情况下,除了cmp al, imm8特殊情况,它仍然是两个字节。

即使如此,test也是出于宏融合的原因(与jle以及Core 2上的类似情况),并且因为根本没有立即数可以通过留下一个插槽来帮助uop缓存密度,如果另一个指令需要更多空间(SnB系列),则可以借用该插槽。

在解码器中将test/jcc宏融合为单个uop

Intel和AMD CPU中的解码器可以在内部macro-fusetestcmp,并将一些条件分支指令合并为一个比较和分支操作。这使您在宏融合发生时每个周期的最大吞吐量为5条指令,而没有宏融合时为4条。(适用于Core 2之后的Intel CPU)
最近的英特尔CPU可以宏融合一些指令(如andadd/sub)以及testcmp,但or不是其中之一。AMD CPU只能用JCC合并testcmp。参见x86_64 - Assembly - loop conditions and out of order,或者直接参考Agner Fog's microarch docs以了解哪些CPU可以宏融合什么。在某些情况下,test可以宏融合,而cmp不能,例如与js
几乎所有简单的ALU操作(按位布尔、加/减等)在单个周期内运行。它们在通过无序执行管道跟踪它们时都有相同的“成本”。Intel和AMD花费晶体管来制造快速执行单元,以便在单个周期内执行加/减/什么。是的,按位ORAND更简单,可能使用的功率略低,但是仍然不能运行得比一个时钟周期快。

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。)

cmptest这样的指令,* 只 * 产生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习惯用法可能来自8080 ORA A,正如评论中指出的那样。
8080's instruction set没有test指令,因此您可以根据ORA AANA 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.anyuops_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,所以它与byteqword相同。~0将是另一种写法。
相关:

  • 什么是当代x86处理器中的指令融合?(微融合和宏融合)。TODO:将测试结果移到那里(并在那里更新我的答案,以修复一些与我当前结果不匹配的东西。
  • x86_64 -汇编-循环条件和无序(哪些指令可以在Sandybridge系列上宏融合)
inb24sb2

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中的值必须来自某个地方,如果它来自一条适当设置标志的指令,那么可以修改代码以避免稍后再次使用另一条指令设置标志。

相关问题