INC指令与ADD 1:有关系吗?

大多数情况下,我现在远离INC和DEC,因为他们做了部分条件代码更新,这可能会导致pipe道中有趣的停顿,而ADD / SUB则不会。 所以在哪里不重要(大多数地方),我使用ADD / SUB来避免摊位。 我只在保持代码小的时候才使用INC / DEC,例如,在一个或两个指令的大小足够大的caching行中进行匹配。 这可能是毫无意义的纳米[字面上!] – 优化,但我在我的编码习惯相当老派。

作者:@Ira Baxter

上面的片段来自INC和DEC指令为什么不影响进位标志?

而且我想问一下,为什么会导致pipe道堵塞,而添加不了? 毕竟,add和inc都会更新标志寄存器。 唯一的区别是inc不更新CF. 但为什么这很重要?

在现代的CPU上, add不会比inc慢(除了间接的代码大小/解码效果),但通常也不会更快,所以你应该更喜欢inc代码大小的原因 。 特别是如果这个select重复多次在同一个二进制文件(例如,如果你是一个编译器 – 作家)。

inc保存1字节(64位模式)或2字节(在32位模式下操作码0x40..F inc r32 / dec r32简称,作为x86-64的REX前缀重新定义)。 这使得总的代码大小差异很小。 这有助于指令caching命中率,iTLB命中率以及必须从磁盘加载的页数。

inc优势:

  • 代码大小直接
  • 不使用立即可以在Sandybridge家庭有uopcaching效果,这可以抵消更好的微融合的add 。 (请参阅微型指南的Sandybridge部分中的Agner Fog表9.1)。Perf计数器可以轻松测量问题阶段的uops,但是难以衡量如何将内容打包到uopcaching和uop-cache读取带宽效果。
  • 在某些情况下,将CF留在未修改的位置是有利的,在CPU上,您可以在没有失速的情况下读取CF。 (不在Nehalem和更早的时候)

在现代CPU中有一个例外: Silvermont / Goldmont / Knight's Landing有效地解码inc / dec作为1 uop,但在分配/重命名(又名issue)阶段扩展到2。 额外的uop合并部分标志。 inc吞吐量仅为每时钟1次,而0.5c(或0.33c Goldmont)独立add r32, imm8是由于旗子合并的uops产生的dep链。

与P4不同的是,寄存器的结果是没有错误标志(见下文),所以当没有任何标志结果使用时,乱序执行将标志合并为等待时间关键path。 (但是OOO窗口比Haswell或Ryzen等主streamCPU要小得多)。在大多数情况下,运行inc作为2个独立的微软可能是Silvermont的一个胜利。 大多数x86指令写入所有的标志而不读取它们,打破了这些标志依赖链。

SMont / KNL在解码和分配/重命名之间有一个队列(参见Intel的优化手册,图16-2 ),因此在问题期间扩展到2个uops可以从解码器中填充气泡(在一个操作数mulpshufb类的指令上)来自解码器的超过1个码,并导致微码的3-7周期失速)。 或者在Silvermont上,只是带有3个以上前缀的指令(包括转义字节和强制性前缀),例如REX +任何SSSE3或SSE4指令。 但是请注意,有一个~28的uop循环缓冲区,所以小循环不会受到这些解码暂停的影响。

inc / dec并不是唯一的解码为1的指令,但是问题是2: push / popcall / ret和3个组件的lea也是这样做的。 所以KNL的AVX512收集说明。 来源: 英特尔优化手册 ,17.1.2乱序引擎(KNL)。 这只是一个小的吞吐量的惩罚(有时甚至不是如果有其他更大的瓶颈),所以通常仍然使用inc进行“通用”调整。


英特尔的优化手册仍然build议一般add 1以上的inc ,以避免部分flag stall的风险。 但是,由于英特尔的编译器默认情况下没有这样做,所以未来的CPU在所有情况下都不会太慢,就像P4一样。

Clang 5.0和Intel的ICC 17(Godbolt)在优化速度( -O3 )时使用inc ,而不仅仅是大小。 -mtune=pentium4使得它们避免了inc / dec ,但是默认的-mtune=generic在P4上并不重要。

ICC17 -xMIC-AVX512 (相当于gcc的-march=knl )确实避免了inc ,这对于Silvermont / KNL来说可能是一个不错的select。 但是使用inc通常不会是一个性能灾难,因此在大多数代码中使用inc / dec可能仍然适合“通用”调优,特别是当标志结果不是关键path的一部分时。


除了Silvermont之外,这是Pentium4剩下的大部分陈旧的优化build议 。 在现代的CPU上,如果你真的读了一个不是由写入任何标志的最后一个insn写的标志,那只会有一个问题。 例如在BigInteger adc循环中。 (在这种情况下,你需要保留CF,所以使用add会破坏你的代码。)

add写入EFLAGS寄存器中的所有条件标志位。 注册重命名使得只写操作非常容易执行乱序操作:请参阅写入后写入和后续读取的危险 。 add eax, 1add ecx, 1可以并行执行,因为它们是完全独立的。 (甚至连Pentium4都将条件标志位重新命名为与EFLAGS的其余部分分开,因为甚至add了中断启用和许多其他位未修改。)

在P4上, incdec取决于所有标志的前一个值 ,所以它们不能彼此并行执行,也不能执行前面的标志设置指令。 (例如, add eax, [mem] / inc ecx使inc等待,直到add ,即使添加的加载在caching中未命中)。 这被称为错误依赖 。 部分标志写入工作通过读取标志的旧值,更新CF以外的位,然后写入完整的标志。

所有其他无序的x86 CPU(包括AMD),都要单独重命名标志的不同部分,以便在内部对除CF之外的所有标志执行只写更新 。 (来源: Agner Fog的微体系结构指南 )。 只有几条指令,如adccmc ,才能真正读取并写入标志。 但是也可以使用(参见下文)。


至less对于Intel P6 / SnB uarch系列, add dest, 1要优于inc dest

  • Memory-destinationadd [rdi], 1可以在Intel Core2和SnB-family上对store和load +进行微熔合 ,所以它是2个融合域uops / 4个非融合域uops。
    inc [rdi]只能将商店微熔,所以是3F / 4U。
    根据Agner Fog的表格,AMD和Silvermont运行memory-dest inc并将其add为一个单一的macros操作/ uop。

    但是要小心使用add [label], 1的uop-cache效果add [label], 1需要一个32位的地址和一个8位的立即数。

  • 在variables计数移位/旋转之前中断对标志的依赖并避免部分标志合并: shl reg, cl由于不幸的CISC历史而对标志具有input依赖性: 如果移位计数为0 。

    在Intel SnB系列中,可变计数转换是3个uops(从Core2 / Nehalem上的1个)。 AFAICT,两个uops读/写标志,一个独立的uop读取regcl ,并写入reg 。 这是一个很奇怪的情况,比吞吐量(1.5c)有更好的延迟(1c +不可避免的资源冲突),并且只有在混合了破坏标志依赖的指令的情况下才能达到最大吞吐量。 ( 我在Agner Fog的论坛上发布了更多这方面的信息)。 尽可能使用BMI2 shlx ; 它是1个,计数可以在任何寄存器中。

    无论如何, inc (写入标志,但离开CF未经修改)之前,variables计数shl离开它对上一次写的CF的错误依赖,并且SnB / IvB可能需要一个额外的uop合并标志。

    Core2 / Nehalem设法避免了对标志的错误解释:Merom每个时钟运行一个6个独立的shl reg,cl指令,每个时钟接近两个移位,同样的性能,cl = 0或cl = 13。 每个时钟优于1的任何事情都certificate没有input对标志的依赖。

    我用shl edx, 2shl edx, 0 (即时计数移位)尝试了循环,但在Core2,HSW或SKL上, decsub之间没有看到速度差异。 我不了解AMD。

更新:Intel P6系列的漂移转换性能的代价是需要避免的大性能坑洼:当指令取决于转换指令的标志结果时: 前端暂停,直到指令退出 (来源: 英特尔优化手册,(第3.5.2.6节:部分标志寄存器暂停) )。 所以shr eax, 2对于英特尔Sandybridge之前的性能来说, shr eax, 2 / jnz是非常灾难性的,我想! 使用shr eax, 2 / test eax,eax / jnz如果你关心Nehalem和更早的话。 英特尔的例子清楚地表明这适用于即时计数的转变,而不仅仅是count = cl

在基于英特尔酷睿微体系结构(这意味着核心2和更高版本)的处理器中,立即移位1由特殊硬件处理,使其不会经历部分标志失速。

英特尔实际上意味着没有立即的特殊操作码,这由一个隐含的1转移。 我认为在编码shr eax,1的两种方法之间存在性能差异shr eax,1与产生只写(部分)标记结果的短编码(使用原始8086操作码D1 /5 ),但是较长的编码( C1 /5, imm8立即1 )直到执行时间没有立即检查0,但没有跟踪无序机械中的标志输出。

由于循环遍历是常见的,但循环遍历每个第二位(或任何其他步骤)是非常罕见的,这似乎是一个合理的deviseselect。 这就解释了为什么编译器喜欢test一个转换的结果,而不是直接使用shr标志结果。

更新:对于SnB系列的可变计数偏移,英特尔的优化手册说:

3.5.1.6可变比特数的旋转和移位

在英特尔微体系架构代号Sandy Bridge中,“ROL / ROR / SHL / SHR reg,cl”指令有三个微操作。 当不需要标志结果时,这些微操作中的一个可能被丢弃,在许多常见的用途中提供更好的性能 。 当这些指令更新随后使用的部分标志结果时,完整的三个微操作stream程必须经过执行和退役stream水线,性能较慢。 在Intel微架构代号Ivy Bridge中,执行完整的三个微操作stream程以使用更新的部分标志结果有额外的延迟。

考虑下面的循环序列:

 loop: shl eax, cl add ebx, eax dec edx ; DEC does not update carry, causing SHL to execute slower three micro-ops flow jnz loop 

DEC指令不会修改进位标志。 因此,SHL EAX,CL指令需要在后续迭代中执行三个微操作stream程。 SUB指令将更新所有标志。 所以用SUB代替DEC将允许SHL EAX, CL执行两个微操作stream程。


术语

标志被读取时部分标志失速发生 ,如果它们发生的话。 P4从来没有部分标志的摊位,因为他们永远不需要合并。 它有错误的依赖关系。

几个答案/评论混淆了术语。 他们描述了一个错误的依赖关系,但是将其称为部分标志失速。 这是由于只写入了一些标志而发生的放缓,但是当部分标志写入必须被合并时,术语“部分标志停顿 ”是在SnB之前的Intel硬件上发生的。 Intel SnB系列CPU插入一个额外的uop来合并标志而不会停顿。 Nehalem和更早的失速〜7个周期。 我不确定AMD CPU的处罚有多大。

(请注意,部分寄存器的惩罚并不总是与部分标志相同,见下文)。

 ### Partial flag stall on Intel P6-family CPUs: bigint_loop: adc eax, [array_end + rcx*4] # partial-flag stall when adc reads CF inc rcx # rcx counts up from negative values towards zero # test rcx,rcx # eliminate partial-flag stalls by writing all flags, or better use add rcx,1 jnz # this loop doesn't do anything useful; it's not normally useful to loop the carry-out back to the carry-in for the same accumulator. # Note that `test` will change the input to the next adc, and so would replacing inc with add 1 

在其他情况下,例如,在完成标志写入之后进行部分标志写入,或者只读取由inc写入的标志即可。 在SnB系列CPU上, inc/dec甚至可以与jccmacros指令保持一致,就像add/sub

在P4之后,英特尔大都放弃了试图让人们用-mtune=pentium4重新编译,或者修改手写的asm以避免严重的瓶颈。 (调整特定的微体系结构将永远是一件事情,但P4是不寻常的,因为过去在以前的CPU上已经很快了 ,因此在现有的二进制代码中很常见。)P4希望人们使用类似RISC的子集x86,也有分支预测提示作为JCC指令的前缀。 (它还有其他严重的问题,比如跟踪caching不够好,弱解码器对跟踪caching的性能不好,更不用说高时钟的整个理念都跑进了功率密度墙。)

当英特尔放弃了P4(netburst uarch)时,他们又回到了P6系列devise(Pentium-M / Core2 / Nehalem),inheritance了从早期P6系列CPU(PPro到PIII)的部分标志/networking犯错误的步骤。 (不是所有关于P4的东西本质上都是坏的,一些想法在Sandybridge中重新出现,但是NetBurst总体上被认为是一个错误。)一些非常CISC指令仍然比多指令替代指令要慢,例如enterloop ,或者bt [mem], reg (因为bt [mem], reg的值会影响到哪个内存地址被使用),但是这些在较老的CPU中都很慢,所以编译器已经避免了这些。

Pentium-M甚至改进了对部分registry的硬件支持(较低的合并惩罚)。 在桑迪布里奇,英特尔保留了部分标志和部分区域的重命名,并且在需要合并的时候效率更高(合并插入的时候没有或者最小的停顿)。 SnB做了重大的内部改变,被认为是一个新的uarch家族,尽pipe它inheritance了Nehalem的很多,还有一些P4的想法。 (但是请注意,SnB的解码后的高速caching不是一个跟踪高速caching,所以它是一个非常不同的解决scheme,以解决netburst的跟踪caching解决的解码器吞吐量/功耗问题。


例如, inc alinc ah可以在P6 / SnB系列CPU上并行运行,但是之后读取eax需要合并

读取完整的脉冲时,PPro / PIII会停止5-6个周期。 核心2 / Nehalem只停留2或3个周期,同时为部分区域插入合并区域,但部分区域仍然是较长的区域。

SnB插入一个合并UOP没有拖延,如旗帜。 英特尔的优化指南说,为了将AH / BH / CH / DH合并到更广泛的registry中,插入合并的uop需要一个完整的发布/重命名周期,在此期间不能分配其他uops。 但是对于low8 / low16,合并的uop是“stream量的一部分”,所以它显然不会引起额外的前端吞吐量处罚,而不是在发出/重命名周期中占用4个槽中的一个。

在IvyBridge(或至lessHaswell)中,Intel放弃了对8位和16位寄存器进行部分寄存器重命名,只保留了8位寄存器(AH / BH / CH / DH)。 读取high8寄存器有额外的延迟。 另外,与Nehalem和更早的(也可能是Sandybridge)不同, setcc al对旧的rax值有依赖性。 有关详细信息,请参阅此HSW / SKL部分注册性能问答 。

(我之前声称Haswell可以把AH和没有UOP合并,但这不是真实的,也不是Agner Fog的指南所说的,我匆匆而过,不幸的是在很多评论和其他文章中重复了我的错误理解。

AMD CPU和Intel Silvermont不重命名部分regs(除了标志),所以mov al, [mem]对eax的旧值有错误的依赖关系。 (稍后阅读完整registry时,上档没有局部区间合并的减速)。


通常情况下,唯一的时间add而不是inc会使你的代码更快的AMD或主stream英特尔是当你的代码实际上依赖于inc的不触摸CF行为。 即通常只会add有助于破坏代码的function ,但请注意上面提到的shl情况,指令读取的是标志,但通常情况下,代码并不关心这种情况,所以这是错误的依赖关系。

如果你真的想离开CF未修改,预先SnB家庭CPU有部分标志的摊位严重问题,但在SnB家庭CPU的合并部分标志的开销是非常低的,所以最好保持使用incdec作为循环条件的一部分时,针对这些CPU,一些展开。 (详情请参阅前面链接的BigInteger adc Q&A)。 如果您不需要在结果上进行分支,则可以使用lea来进行算术运算,而不会影响标志。

根据指令的CPU执行情况,部分寄存器更新可能导致失速。 根据Agner Fog的优化指南(第62页 )

由于历史原因, INCDEC指令保持进位标志不变,而其他算术标志被写入。 这会导致对标志的先前值的错误依赖,并花费额外的μop。 为避免这些问题,build议您始终使用ADDSUB而不是INCDEC 。 例如, INC EAX应该被ADD EAX,1replaceADD EAX,1

有关“部分标志失速”的第83页和“部分标志失速”的第100页。