从Ira Baxter answer on, Why do the INC and DEC instructions not affect the Carry Flag (CF)?
通常情况下,我现在远离INC
和DEC
,因为它们会进行部分条件代码更新,这可能会导致管道中出现有趣的停顿,而ADD
/SUB
不会。因此,在不重要的地方(大多数地方),我使用ADD
/SUB
来避免停顿。我使用INC
/DEC
只有当保持代码小的问题,例如,在一个缓存行中,一个或两个指令的大小足以造成差异的问题。这可能是毫无意义的纳米[字面意思!]-优化,但我的编码习惯相当老派。
我想问为什么它会导致管道中的停顿,而add不会?毕竟,ADD
和INC
都会更新标志寄存器。唯一的区别是INC
不会更新CF
。但这有什么关系呢?
2条答案
按热度按时间gv8xihay1#
更新:桤木Lake上的效率内核是Gracemont,并将
inc reg
作为单个uop运行,但仅为1/时钟,而不是add reg, 1
(https://uops.info/)为4/时钟。这可能是像P4一样对FLAGS的错误依赖; uops.info测试没有尝试添加dep-breaking指令。除了TL:DR,我没有更新这个答案的其他部分。Update 2:One 2021 source claims Ice Lake不能宏融合
inc
/dec
(或内存操作数指令)与jcc
。如果这是真的,那么在循环底部等情况下,使用sub ecx, 1
/jnz
而不是传统的dec ecx
/jnz
。但是Agner Fog和uiCA都说ICL仍然可以融合inc
/dec
。它可能在2019年至2021年之间的微码更新中发生了变化,但testing on Tiger Lake显示它并不这样工作。TL:DR/针对现代CPU的建议:可能使用
add
;英特尔桤木Lake的E核与“通用”调优相关,似乎运行inc
很慢。除了桤木Lake和更早的Silvermont-family,使用
inc
,除非有内存目的地;这在主流英特尔或任何AMD上都没问题。(例如,像gcc-mtune=core2
、-mtune=haswell
或-mtune=znver1
)。inc mem
与英特尔P6 /SnB系列上的add
;负载不能微熔断。如果您关心Silvermont-family(包括Xeon Phi中的KNL,以及一些上网本,Chromebook和NAS服务器),可能会避免
inc
。add 1
在64位代码中只需要1个额外字节,在32位代码中只需要2个额外字节。但这并不是性能灾难(只是在本地使用了1个额外的ALU端口,没有创建虚假的依赖关系或大的停顿),所以如果你不太关心SMont,那就不要担心它。写CF而不是不修改它可能对其他可能受益于CF深度破坏的周围代码有用,例如。轮班。见下文。
如果你想在不触及 any 标志的情况下进行inc/dec,
lea eax, [rax+1]
运行效率很高,并且代码大小与add eax, 1
相同。(通常在比add/inc更少的可能执行端口上,所以当销毁FLAGS不是问题时,add/inc更好。https://agner.org/optimize/)在现代CPU上,
add
永远不会 * 比inc
慢 *(除了间接的代码大小/解码效果),但通常它也不会更快,因此出于代码大小的原因,您应该首选inc
。特别是如果这个选择在同一个二进制文件中重复了很多次(例如:如果你是一个编译器作者)。inc
保存1个字节(64位模式)或2个字节(32位模式下的操作码0x40..Finc r32
/dec r32
缩写,重新用作x86-64的雷克斯前缀)。这在总代码大小上产生了很小的百分比差异。这有助于提高指令缓存命中率、iTLB命中率和必须从磁盘加载的页面数量。inc
的优点:*代码大小直接
add
更好的微融合。性能计数器可以很容易地测量问题阶段的uop,但是很难测量uop缓存和uop缓存读取带宽的影响。inc
之后读取CF而不会停止。(不适用于Nehalem和更早)。现代CPU中有一个例外:Silvermont/Goldmont/Knight's Landing将
inc
/dec
有效地解码为1 uop,但在分配/重命名(aka issue)阶段扩展为2。额外的uop合并部分标志。inc
的吞吐量每时钟仅为1,而0.5c(或0.33c Goldmont),对于独立的add r32, imm8
,因为标志合并uops创建了dep链。与P4不同,寄存器结果在标志上不具有错误的dep(见下文),因此当没有任何东西使用标志结果时,无序执行将标志合并从延迟关键路径中移除。(但是OOO窗口比Haswell或Ryzen这样的主流CPU要小得多。)在大多数情况下,将
inc
作为两个独立的uop运行可能是Silvermont的胜利;大多数x86指令写入所有标志而不阅读它们,从而破坏了这些标志相关性链。SMont/KNL在decode和allocate/rename之间有一个队列(参见Intel的优化手册,图16-2),因此在问题期间扩展到2个uop可以填充解码暂停的气泡(在像单操作数x1m3 - 7n1x或
pshufb
这样的指令上,这些指令从解码器产生超过1个uop,并导致3-7个周期的微码暂停)。或者在Silvermont上,只有一条指令有3个以上的前缀(包括转义字节和强制前缀),例如雷克斯+任何SSSE 3或SSE 4指令。但请注意,有一个~28 uop的循环缓冲区,所以小循环不会受到这些解码停顿的影响。inc
/dec
不是唯一解码为1但发出为2的指令:具有3个组件的push
/pop
、call
/ret
和lea
也可以实现此功能。KNL的AVX 512收集指令也是如此。来源:英特尔优化手册,17.1.2 Out-of-Order Engine(KNL)。这只是一个很小的吞吐量损失(有时甚至没有,如果其他东西是一个更大的瓶颈),所以通常仍然可以使用inc
进行“通用”调优。英特尔的优化手册仍然建议使用
add 1
而不是inc
,以避免部分标志停顿的风险。但由于英特尔的编译器默认情况下不会这样做,因此未来的CPU不太可能像P4那样在所有情况下都使inc
变慢。Clang 5.0和英特尔的ICC 17(在Godbolt上)在优化速度(
-O3
)时确实使用了inc
,而不仅仅是大小。-mtune=pentium4
使它们避免inc
/dec
,但默认的-mtune=generic
不会对P4产生太大影响。ICC 17
-xMIC-AVX512
(相当于gcc的-march=knl
)确实避免了inc
,这对于Silvermont / KNL来说可能是一个不错的选择。但是使用inc
通常不会造成性能灾难,因此在大多数代码中使用inc
/dec
可能仍然适合于“通用”调优,特别是当标志结果不是关键路径的一部分时。除了Silvermont,这是从奔腾4遗留下来的大多数过时的优化建议。在现代CPU上,只有当您实际读取的标志不是由写入 any 标志的最后一个insn写入的标志时才会出现问题。e.g. in BigInteger
adc
loops.(在这种情况下,您需要保留CF,以便使用add
会破坏代码。add
将所有条件标志位写入EFLAGS寄存器。寄存器重命名使乱序执行的只写操作变得容易:参见写后写和读后写危险。add eax, 1
和add ecx, 1
可以并行执行,因为它们彼此完全独立。(偶数奔腾4重命名条件标志位与EFLAGS的其余部分分开,因为偶数add
使中断启用,并且许多其他位未修改。在P4上,
inc
和dec
依赖于所有标志的前一个值,因此它们不能与彼此或前面的标志设置指令并行执行。(例如add eax, [mem]
/inc ecx
使inc
等待直到add
之后,即使add的加载在缓存中未命中。部分标志写入通过阅读标志的旧值、更新除CF之外的位、然后写入完整标志来工作。所有其他乱序的x86 CPU(包括AMD的),分别重命名标志的不同部分,因此它们在内部对除CF之外的所有标志进行只写更新。(来源:Agner Fog's microarchitecture guide)。只有少数指令,如
adc
或cmc
,真正读取然后写入标志。还有shl r, cl
(见下文)。add dest, 1
优于inc dest
的情况,至少对于英特尔P6/SnB系列:*内存目标:
add [rdi], 1
可以在Intel Core 2和SnB系列上对存储和加载+添加进行微融合,因此它是2个融合域uop/ 4个非融合域uop。inc [rdi]
只能对存储进行微熔断,所以是3F / 4U。根据Agner Fog的表,AMD和Silvermont以相同的方式运行内存dest
inc
和add
,作为单个宏操作/ uop。但是要注意
add [label], 1
的uop缓存效应,它需要一个32位地址和一个8位立即数来实现相同的uop。*在变量计数移位/旋转之前,打破对标志的依赖,避免部分标志合并:
shl reg, cl
对标志有输入依赖性,因为不幸的CISC历史:it has to leave them unmodified if the shift count is 0。在英特尔SnB系列上,可变计数移位为3 uops(在Core 2/Nehalem上为1)。AFAICT,两个uop读/写标志,一个独立的uop读
reg
和cl
,写reg
。这是一个奇怪的情况,具有更好的延迟(1c +不可避免的资源冲突)比吞吐量(1.5c),并且只有在与打破对标志的依赖关系的指令混合时才能实现最大吞吐量。(我在Agner Fog的论坛上发布了更多关于这一点的信息)。尽可能使用BMI 2shlx
;它是1uop,计数可以在任何寄存器中。无论如何,在变量计数
shl
之前的inc
(写入标志,但不修改CF
)使其对最后写入CF的内容具有false依赖性,并且在SnB/IvB上可能需要额外的uop来合并标志。Core 2/Nehalem设法避免甚至错误的标志:Merom运行6个独立
shl reg,cl
指令的循环,每个时钟几乎有两个移位,与cl=0或cl=13的性能相同。任何优于每时钟1的值都证明对标志没有输入依赖性。我尝试使用
shl edx, 2
和shl edx, 0
(立即计数移位)进行循环,但在Core 2、HSW或SKL上没有看到dec
和sub
之间的速度差异。我不知道AMD。更新:英特尔P6系列的良好换档性能是以一个大的性能坑洞为代价的,你需要避免:当指令取决于移位指令的标志结果时:前端 * 暂停,直到指令 * 退出 *。(源代码:英特尔的优化手册,(第www.example.com节3.5.2.6:部分标志寄存器暂停)。因此,我猜
shr eax, 2
/jnz
在Intel pre-Sandybridge上的性能是相当灾难性的!如果您关心Nehalem和更早的版本,请使用shr eax, 2
/test eax,eax
/jnz
。英特尔的例子清楚地表明,这适用于立即计数移位,而不仅仅是count=cl
。在基于Intel Core微架构的处理器中[这意味着Core 2和更高版本],立即移位1由特殊硬件处理,因此它不会经历部分标志停顿。
Intel实际上是指没有立即数的特殊操作码,它通过隐式
1
进行移位。我认为shr eax,1
的两种编码方式之间存在性能差异,短编码(使用原始的8086操作码D1 /5
)产生一个只写(部分)标志结果,但较长的编码(C1 /5, imm8
与立即数1
)直到执行时间才检查其立即数是否为0,但不跟踪乱序机器中的标志输出。由于循环位是常见的,但循环每2位(或任何其他步幅)是非常罕见的,这似乎是一个合理的设计选择。这解释了为什么编译器喜欢
test
移位的结果,而不是直接使用shr
的标志结果。更新:对于SnB系列的可变计数移位,英特尔的优化手册说:
3.5.1.6可变位数旋转和移位
在Intel微架构代码名桑迪Bridge中,“ROL/ROR/SHL/SHR reg,cl”指令具有三个微操作。当不需要标志结果时,可以丢弃这些微操作之一,从而在许多常见用途中提供更好的性能。当这些指令更新随后使用的部分标志结果时,完整的三个微操作流必须经过执行和引退流水线,从而经历较慢的性能。在英特尔微架构代码名Ivy Bridge中,执行完整的三个微操作流以使用更新的部分标志结果具有额外的延迟。
考虑下面的循环序列:
DEC指令不修改进位标志。因此,SHL EAX,CL指令需要在后续迭代中执行三个微操作流。SUB指令将更新所有标志。因此,将
DEC
替换为SUB
将允许SHL EAX, CL
执行两个微操作流。术语
部分标志暂停发生在读取标志时,如果它们发生的话。P4从来没有部分标志暂停,因为它们从来不需要合并。相反,它具有假依赖性。
几个回答/评论混淆了术语。他们描述了一个假依赖,但随后称之为部分标志停顿。这是一个缓慢的情况,因为只写一些标志,但术语“部分标志停止”是发生在前SnB英特尔硬件时,部分标志写入必须合并。英特尔SnB系列CPU插入一个额外的uop来合并标志而不会停止。Nehalem和更早的失速约7个周期。我不知道AMD CPU的处罚有多大。
(Note部分寄存器惩罚不总是与部分标志相同,见下文)。
在其他情况下,例如部分标志写入之后是完整标志写入,或者仅读取由
inc
写入的标志,都是可以的。在SnB系列CPU上,inc/dec
甚至可以与jcc
宏融合,与add/sub
相同。在P4之后,英特尔大多放弃了让人们用
-mtune=pentium4
重新编译或修改手写的asm以避免严重的瓶颈。(针对特定的微架构进行调优始终是一件事,但是P4不寻常地弃用了许多在以前的CPU上速度很快的东西,因此在现有的二进制文件中很常见。)P4希望人们使用类似RISC的x86子集,并且也有分支预测提示作为JCC指令的前缀。(它还有其他严重的问题,比如跟踪缓存不够好,以及弱解码器,这意味着跟踪缓存未命中的性能很差。更不用说整个时钟非常高的哲学撞上了功率密度墙。)当英特尔放弃P4(NetBurst uarch)时,他们回到了P6系列设计(Pentium-M /Core 2/ Nehalem),这些设计继承了早期P6系列CPU(PPro到PIII)的部分标志/部分reg处理,这些处理早于netburst失误。(并不是所有关于P4的东西本质上都不好,有些想法在Sandybridge中重新出现,但总体上NetBurst被广泛认为是一个错误。
enter
、loop
或bt [mem], reg
(因为reg的值会影响使用哪个内存地址),但这些在旧CPU中都很慢,因此编译器已经避免了它们。奔腾-M甚至改进了对部分规则的硬件支持(降低合并惩罚)。在Sandybridge中,英特尔保留了部分标志和部分reg重命名,并在需要合并时使其更加高效(合并uop插入时没有或最小化停顿)。SnB进行了重大的内部变化,被认为是一个新的uarch家族,尽管它继承了Nehalem的很多东西,以及P4的一些想法。(但是请注意,SnB的decoded-uop缓存 * 不是 * 跟踪缓存,所以它是NetBurst跟踪缓存试图解决的解码器吞吐量/功耗问题的一个非常不同的解决方案。
例如,
inc al
和inc ah
可以在P6/SnB系列CPU上并行运行,但之后阅读eax
需要合并。阅读完整寄存器时,PPro/PIII停转5-6个周期。Core 2/Nehalem在为部分regs插入合并uop时仅停止2或3个周期,但部分标志仍然是更长的停止。
SnB插入一个合并的uop而不停顿,就像标记一样。英特尔的优化指南指出,为了将AH/BH/CH/DH合并到更宽的reg中,插入合并uop需要整个发布/重命名周期,在此期间不能分配其他uop。但是对于low 8/low 16,合并uop是“流的一部分”,所以除了占用发布/重命名周期中的4个插槽之一之外,它显然不会导致额外的前端吞吐量损失。
在IvyBridge(或至少Haswell)中,英特尔放弃了对low 8和low 16寄存器的部分寄存器重命名,只保留了对high 8寄存器(AH/BH/CH/DH)的重命名。阅读high 8寄存器具有额外的延迟。此外,
setcc al
对rax的旧值有一个false依赖,这与Nehalem和更早的版本(可能还有Sandybridge)不同。详情请参见this HSW/SKL partial-register performance Q&A。(我之前声称Haswell可以合并AH而不使用uop,但这不是真的,Agner Fog的指南也不是这么说的。我浏览得太快了,不幸的是在很多评论和其他帖子中重复了我的错误理解。)
AMD CPU和Intel Silvermont不会重命名部分regs(除了flag),因此
mov al, [mem]
对eax的旧值有一个false依赖关系。(好处是在以后阅读完整reg时,部分reg合并不会变慢。)通常情况下,
add
而不是inc
将使您的代码在AMD或主流英特尔上更快的唯一时间是当您的代码实际上依赖于inc
的不接触CF行为时。也就是说通常add
只在它会破坏你的代码时才有帮助,但请注意上面提到的shl
情况,其中指令读取标志,但通常你的代码并不关心它,所以它是一个假依赖项。如果你真的想不修改CF,pre SnB系列CPU有严重的部分标志暂停问题,但在SnB系列上,CPU合并部分标志的开销非常低,所以最好在针对这些CPU时继续使用
inc
或dec
作为循环条件的一部分,并进行一些展开。(详情请参见我之前链接的BigIntegeradc
Q&A)。如果您不需要在结果上分支,那么使用lea
进行算术运算而不影响标志是很有用的。Skylake没有部分标志合并开销
更新:Skylake根本没有partial-flag合并uops:CF只是一个独立于FLAGS的寄存器。需要两个部分的指令(如
cmovbe
)分别读取两个输入。这使得cmovbe
成为2-uop指令,但Skylake上的大多数其他cmovcc
指令都是1-uop。看看什么是部分国旗失速?.adc
only 读取CF,因此它可以在Skylake上进行单uop,而不会与同一循环中的inc
或dec
进行任何交互。(TODO:重写本答案的前面部分。)
sh7euo9m2#
根据指令的CPU实现,部分寄存器更新可能导致停顿。根据Agner Fog的优化指南第62页
由于历史原因,
INC
和DEC
指令保持进位标志不变,而写入其他算术标志。这会导致对标志的先前值的错误依赖,并消耗额外的μop。为了避免这些问题,建议您始终使用ADD
和SUB
,而不是INC
和DEC
。例如,INC EAX
应替换为ADD EAX,1
。另见第83页“部分标志失速”和第100页“部分标志失速”。