INC指令与ADD 1:是否有关系?

从Ira Baxter回答,为什么INC和DEC指令不会影响进位标志(CF)?

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

我想问,为什么它会导致管道中的堵塞,而添加不会? 毕竟, ADDINC更新标志寄存器。 唯一的区别是INC不更新CF 但为什么它很重要?


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

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

inc优势:

  • 代码大小直接
  • 不使用立即可以对Sandybridge系列产生uop缓存效果,这可以抵消更好的微融合add 。 (请参阅微型指南的Sandybridge部分中的Agner Fog表9.1)。Perf计数器可以轻松测量问题阶段的uops,但很难衡量事物如何打包进uop缓存和uop-cache读取带宽效果。
  • 离开CF未修改是一个优势,在某些情况下,在CPU上,你可以阅读后CF inc没有失速。 (不在Nehalem和更早的时候。)

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

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

    SMont / KNL具有解码之间的队列和分配/重命名(见Intel的优化手册,图16-2)中的问题,从而扩展到2个微指令可以填充从解码档位气泡(像一个操作数的指令mul ,或pshufb ,即产生来自解码器的超过1个uop并导致微码的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来进行“通用”调整。


    英特尔优化手册还建议add 1inc在一般情况下,避免部分标志摊位风险。 但由于英特尔的编译器不会做,默认情况下,这不是太可能在未来的处理器将让inc在所有情况下慢,像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来说可能是一个不错的选择。 但使用inc通常不会是性能灾难,所以在大多数代码中使用inc / dec可能仍然适合“通用”调优,尤其是当标志结果不是关键路径的一部分时。


    除了Silvermont之外,这是Pentium4遗留下来的大部分陈旧的优化建议 。 在现代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 ,即使添加的加载在缓存中未命中)。 这称为错误依赖 。 部分标志写入工作是通过读取标志的旧值,更新CF以外的位,然后写入完整标志。

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


    至少对于Intel P6 / SnB uarch系列, add dest, 1情况优于inc dest

  • 内存目的地add [rdi], 1可以在Intel Core2和SnB系列上对商店和加载+进行微融合,因此它是2个融合域uops / 4个非融合域uops。
    inc [rdi]只能将商店进行微熔合,因此它是3F / 4U。
    根据Agner Fog的表格,AMD和Silvermont运行memory-dest incadd相同的宏,作为单个宏操作/ uop。

    但要注意add [label], 1的uop-cache效果add [label], 1需要一个32位地址和一个8位立即数的同一个uop。

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

    在Intel SnB系列中,可变计数偏移是3个uops(从Core2 / Nehalem上的1个)。 AFAICT,两个uops读/写标志,一个独立的uop读取regcl ,并写入reg 。 这是一个奇怪的例子,它具有比吞吐量(1.5c)更好的延迟(1c +不可避免的资源冲突),并且只有在与标志上的依赖性相关的指令混合时才能实现最大吞吐量。 (我在Agner Fog的论坛上发布了更多关于此的内容)。 尽可能使用BMI2 shlx ; 它是1个,计数可以在任何寄存器中。

    无论如何, inc (编写标志,但保留CF未修改)在variable-count shl离开之前,对于上次写入CF的任何错误依赖关系,并且在SnB / IvB上可能需要额外的uop来合并标志。

    Core2 / Nehalem设法避免对标志的错误解析:Merom每时钟运行一次6个独立的shl reg,cl指令,每个时钟接近两个班次,性能与cl = 0或cl = 13相同。 每个时钟优于1的任何事情都证明没有输入依赖于标志。

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

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

    在基于英特尔酷睿微体系结构的处理器[这意味着Core 2及更高版本]中,立即移位1由特殊硬件处理,使其不会出现部分标志失速。

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

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

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

    3.5.1.6可变位计数旋转和移位

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

    考虑下面的循环序列:

    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指令需要在后续迭代中执行三个微操作流程。 SUB指令将更新所有标志。 所以用SUB代替DEC将允许SHL EAX, CL执行两个微操作流程。


    术语

    当标志被读取时部分标志失速发生 ,如果它们发生的话。 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甚至可以与jcc宏指令熔合,与add/sub相同。

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

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

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


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

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

    SnB插入一个合并的UOP而不会拖延,就像标志一样。 英特尔的优化指南表示,为了将AH / BH / CH / DH合并到更广泛的章节中,插入合并的uop需要一个完整的发布/重命名周期,在此期间不能分配其他uops。 但是对于low8 / low16,合并的uop是“流量的一部分”,所以它显然不会导致额外的前端吞吐量损失,除了在问题/重命名周期中占用4个槽位之一。

    在IvyBridge(或至少Haswell)中,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的旧值有错误的依赖关系。 (稍后阅读完整注册表时,上档没有部分注册合并速度减慢。)


    通常情况下,唯一的一次add ,而不是inc将会使你的代码基于AMD更快或主流的英特尔是当你的代码实际上取决于对doesn't触摸-CF行为inc 。 即通常只会在破坏代码时add帮助 ,但请注意上面提到的shl情况,其中指令读取标志,但通常您的代码不关心这种情况,所以这是错误的依赖关系。

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


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

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

    有关“部分标志停顿”的第83页和“部分标志停止”的第100页。

    链接地址: http://www.djcxy.com/p/15067.html

    上一篇: INC instruction vs ADD 1: Does it matter?

    下一篇: Tracing call stack in disassembled code