更新:Alder Lake上的
效能核心是
Gracemont,并以单个uop运行
inc reg
,但每个时钟周期只有1次,而
add reg, 1
则为每个时钟周期4次(
https://uops.info/)。这可能是对FLAGS的错误依赖,就像P4一样;uops.info的测试没有尝试添加一个打破依赖关系的指令。除了TL:DR之外,我还没有更新其他部分的答案。
更新2:
一份2021年的消息来源声称,冰湖处理器无法将
inc
/
dec
(或带有内存操作数的指令)与
jcc
宏融合。如果这是真的,在循环底部等情况下,应该使用
sub ecx, 1
/
jnz
代替传统的
dec ecx
/
jnz
。但
Agner Fog和
uiCA都表示ICL仍然可以融合
inc
/
dec
。可能在2019年和2021年之间的微码更新中发生了变化,但
在Tiger Lake上的测试表明它不起作用。
TL:DR/现代CPU的建议:大概使用
add
;英特尔Alder Lake的E-cores适用于“通用”调优,似乎运行
inc
较慢。
除了Alder Lake和早期的Silvermont系列,使用
inc
,除非目标是内存;这在主流的英特尔或任何AMD上都没问题(例如像gcc的
-mtune=core2
,
-mtune=haswell
或
-mtune=znver1
)。在英特尔P6/SnB系列上,
inc mem
比
add
多一个微操作;加载无法微融合。
如果你关心Silvermont系列(包括Xeon Phi中的KNL以及一些上网本、Chromebook和NAS服务器),可能要避免使用
inc
。在64位代码中,
add 1
只增加1个额外字节,在32位代码中增加2个字节。但这不是性能灾难(只是局部使用了1个额外ALU端口,不会产生错误依赖或大的停顿),所以如果你对SMont不太关心,就不用担心它。
写CF而不是保持不变可能在其他周围的代码中有用,这些代码可能从CF dep-breaking中受益,例如移位操作。请参见下文。
如果您想要进行增加/减少操作而不触及任何标志位,
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..F 的
inc r32
/
dec r32
短格式,被重新用作 x86-64 的 REX 前缀)。这在总代码大小中占据了很小的比例差异。这有助于指令缓存命中率、iTLB命中率和需要从磁盘加载的页面数量。
inc
的优点包括:
- code-size直接
- 不使用立即数可以对Sandybridge系列产生uop缓存效应,这可能抵消
add
的更好微融合。(参见{{link1:Agner Fog在他的微体系结构指南中Sandybridge部分的表9.1。)性能计数器可以轻松测量发射阶段uop,但很难测量如何打包到uop缓存和uop缓存读取带宽效应。
- 在某些情况下,保持CF不变是有利的,尤其是在可以在
inc
之后无需停顿即可读取CF的CPU上。(对于Nehalem及更早版本则不适用。)
在现代CPU中有一个例外:Silvermont/Goldmont/Knight's Landing 在分配/重命名(也称为发出)阶段以1个uop高效地解码inc
/dec
,但扩展为2个。额外的uop合并部分标志。由于合并标志uop创建的依赖链, inc
吞吐量每个时钟只有1个,而独立add r32, imm8
为0.5c(或0.33c Goldmont)。
与P4不同,寄存器结果对标志没有错误依赖(见下文),因此当没有任何东西使用标志结果时,乱序执行可以将标志合并从延迟关键路径中移除。(但是乱序执行窗口要比Haswell或Ryzen等主流CPU小得多。)在大多数情况下,将inc
作为2个单独的uop运行对于Silvermont来说可能是一种胜利;大多数x86指令在不读取它们的情况下写入所有标志位,打破这些标志依赖链。
SMont/KNL在解码和分配/重命名之间有一个队列(参见
Intel的优化手册,图16-2),因此在发出期间扩展到2个微操作可以填充解码停顿引起的气泡(例如单操作数
mul
或
pshufb
指令,从解码器产生多于1个微操作并导致3-7个周期的微码停顿)。或者在Silvermont上,只需具有超过3个前缀(包括转义字节和强制前缀)的指令,例如REX + 任何SSSE3或SSE4指令。但请注意,有一个约28个微操作的循环缓冲区,因此小循环不会受到这些解码停顿的影响。
inc
/dec
不是唯一的指令,解码为1但发出为2的指令: push
/pop
,call
/ret
,以及具有3个组件的lea
也是如此。KNL的AVX512 gather指令也是如此。来源:Intel的优化手册,17.1.2 乱序引擎(KNL)。这只是一个小的吞吐量惩罚(有时甚至不是,如果其他因素成为更大的瓶颈),所以通常仍然可以使用inc
进行“通用”调优。
Intel的优化手册仍然推荐在一般情况下使用
add 1
而不是
inc
,以避免部分标志位停顿的风险。但由于Intel的编译器默认不这样做,未来的CPU很可能不会像P4那样在所有情况下都使
inc
变慢。
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上,只有在实际读取一个不是由最后一条写入任何标志位的指令写入的标志位时才会出现问题。例如,在BigInteger的adc循环中(需要保留CF以便使用add不会破坏代码)。
add指令会写入EFLAGS寄存器中的所有条件标志位。寄存器重命名使得写入操作变得容易,适用于乱序执行:参见写后写和写后读的危险。add eax, 1和add ecx, 1可以并行执行,因为它们彼此完全独立。(即使是Pentium4也将条件标志位与其他EFLAGS位分开重命名,因为即使add指令也不会修改中断使能位和许多其他位。)
在P4上,
inc
和
dec
取决于所有标志位的先前值,因此它们不能与彼此或先前设置标志位的指令并行执行。(例如:
add eax, [mem]
/
inc ecx
会使
inc
等待
add
之后执行,即使add的加载未命中缓存。)这被称为伪依赖。部分标志位写入通过读取旧的标志位值,更新除CF以外的位,然后写入完整的标志位。
所有其他乱序执行的x86 CPU(包括AMD的CPU),会单独重命名标志位的不同部分,因此在内部对除CF以外的所有标志位进行只写更新。(来源:
Agner Fog的微架构指南)。只有少数指令,如
adc
或
cmc
,真正读取然后写入标志位。但是
shl r, cl
也是如此(见下文)。
对于英特尔P6/SnB微架构家族,add dest, 1
优于inc dest
的情况:
- 内存目标:在英特尔Core2和SnB系列上,
add [rdi], 1
可以将存储和加载+加法融合在一起,因此它是2个融合域uop / 4个非融合域uop。
inc [rdi]
只能将存储融合在一起,所以是3F / 4U。
根据Agner Fog的表格,AMD和Silvermont运行内存目标的inc
和add
相同,作为单个宏操作/uop。
但要注意,add [label], 1
需要一个32位地址和一个8位立即数来执行相同的uop。
在进行变量计数的移位/旋转之前,为了打破对标志位的依赖并避免部分标志位合并:由于不幸的CISC历史原因,
shl reg, cl
对标志位有输入依赖,如果移位计数为0,则必须保持它们不变。
在Intel SnB系列上,变量计数的移位需要3个微操作(与Core2/Nehalem相比增加了1个)。据我所知,其中两个微操作读/写标志位,一个独立的微操作读取
reg
和
cl
,并写入
reg
。这是一种奇怪的情况,它具有更好的延迟(1个周期+不可避免的资源冲突),而吞吐量(1.5个周期)只能在与打破对标志位依赖的指令混合使用时达到最大吞吐量。(我在Agner Fog的论坛上发表了更多相关内容)。尽可能使用BMI2的
shlx
指令;它只需要1个微操作,并且计数可以在任何寄存器中。
无论如何,在变量计数的shl之前,inc(写入标志位但不修改CF)会使其对最后一次写入CF的内容产生错误依赖,并且在SnB/IvB上可能需要额外的uop来合并标志位。
Core2/Nehalem甚至可以避免对标志位的错误依赖:Merom以近乎每个时钟周期两次移位的速度运行一个由6个独立的shl reg,cl指令组成的循环,当cl=0或cl=13时性能相同。任何超过每个时钟周期1次的速度都证明了对标志位没有输入依赖。
我尝试了使用shl edx, 2和shl edx, 0(立即计数移位)的循环,但在Core2、HSW或SKL上没有看到dec和sub之间的速度差异。我不清楚AMD的情况。
更新:在Intel P6系列上的良好移位性能是以一个巨大的性能陷阱为代价的,你需要避免:当一条指令依赖于移位指令的标志结果时,前端将停滞直到该指令被撤销。(来源:
Intel的优化手册,(3.5.2.6节:部分标志寄存器停滞))。所以
shr eax, 2
/
jnz
在Intel Sandybridge之前的处理器上对性能来说相当灾难!如果你关心Nehalem及更早的处理器,请使用
shr eax, 2
/
test eax,eax
/
jnz
。Intel的示例清楚地说明了这适用于立即计数移位,而不仅仅是count=
cl
。
基于英特尔Core微架构的处理器[即Core 2及更高版本],移位操作数为1由特殊硬件处理,以避免部分标志寄存器停顿。
实际上,Intel指的是没有立即数的特殊操作码,它通过隐式的1进行移位。我认为在编码shr eax,1
的两种方式之间存在性能差异,使用短编码(使用原始8086操作码D1 /5
)会产生一个只写(部分)标志结果,但较长的编码(C1 /5, imm8
,带有立即数1
)直到执行时才检查其立即数是否为0,但不跟踪乱序机制中的标志输出。
由于循环遍历位是常见的,但是循环遍历每2个位(或任何其他步幅)非常罕见,所以这似乎是一个合理的设计选择。这解释了为什么编译器喜欢对移位的结果进行test
而不是直接使用shr
的标志结果。
更新:对于SnB系列的可变计数移位,Intel的优化手册中提到:
3.5.1.6 Variable Bit Count Rotation and Shift
In Intel microarchitecture code name Sandy Bridge, The “ROL/ROR/SHL/SHR reg, cl” instruction has three micro-ops. When the flag result is not needed, one of these micro-ops may be discarded, providing
better performance in many common usages. When these instructions update partial flag results that are subsequently used, the full three micro-ops flow must go through the execution and retirement pipeline,
experiencing slower performance. In Intel microarchitecture code name Ivy Bridge, executing the full three micro-ops flow to use the updated partial flag result has additional delay.
Consider the looped sequence below:
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
The DEC instruction does not modify the carry flag. Consequently, the
SHL EAX, CL instruction needs to execute the three micro-ops flow in
subsequent iterations. The SUB instruction will update all flags. So
replacing DEC
with SUB
will allow SHL EAX, CL
to execute the two
micro-ops flow.
术语
部分标志位停顿发生在读取标志位时,如果有的话。P4从不出现部分标志位停顿,因为它们永远不需要合并。相反,它会出现伪依赖。
一些答案/评论混淆了术语。他们描述了一个伪依赖,但却称之为部分标志位停顿。这是由于只写入了一些标志位而导致的减速,但术语“部分标志位停顿”是指在Sandy Bridge之前的英特尔硬件上发生的部分标志位写入必须合并的情况。Intel Sandy Bridge系列CPU会插入额外的微操作来合并标志位而不会停顿。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上,{{link1:
inc/dec
甚至可以与
jcc
宏融合,与
add/sub
一样}}。
在P4之后,英特尔基本放弃了试图让人们重新编译使用
-mtune=pentium4
或修改手写汇编代码以避免严重瓶颈的尝试。(针对特定微架构的调优始终存在,但
P4在弃用了许多在先前CPU上表现良好的功能后有些不寻常,因此在现有二进制文件中很常见。)P4希望人们使用类似RISC的x86子集,并且还为JCC指令提供分支预测提示作为前缀。(它还有其他严重问题,例如追踪缓存不够好,弱解码器导致追踪缓存未命中时性能较差。更不用说高频率时钟的整体理念遇到了功耗密度限制。)
当英特尔放弃P4(NetBurst微架构)后,他们回归到了P6系列设计(Pentium-M / Core2 / Nehalem),这些设计继承了早期P6系列CPU(PPro到PIII)的部分标志位/部分寄存器处理方式,而这些早期设计在NetBurst失误之前就已存在。(并非P4的所有方面都是不好的,一些想法在Sandybridge中重新出现,但总体上,NetBurst被广泛认为是一个错误。)一些非常CISC的指令仍然比多指令替代方案慢,例如
enter
,
loop
或
bt [mem], reg
(因为reg的值会影响使用的内存地址),但这些在旧的CPU中都很慢,所以编译器已经避免使用它们。
Pentium-M甚至改进了对部分寄存器的硬件支持(降低合并惩罚)。在Sandybridge中,英特尔保留了部分标志和部分寄存器重命名,并在需要合并时使其更加高效(插入无或最小停顿的合并微操作)。SnB进行了重大的内部变化,被认为是一个新的微架构系列,尽管它继承了很多来自Nehalem的东西,以及一些来自P4的思想。(但请注意,SnB的解码微操作缓存不是一个跟踪缓存,因此它是对NetBurst的跟踪缓存试图解决的解码吞吐量/功耗问题的非常不同的解决方案。)
例如,
inc al
和
inc ah
可以在P6/SnB系列CPU上并行运行,但之后读取
eax
需要合并。
PPro/PIII在读取完整寄存器时会停顿5-6个周期。Core2/Nehalem在插入用于部分寄存器的合并微操作时只会停顿2或3个周期,但部分标志位仍然会有较长的停顿时间。
SnB在不停顿的情况下插入了一个合并微操作,就像对标志位一样。英特尔的优化指南表示,将AH/BH/CH/DH合并到更宽的寄存器中,插入合并微操作需要整个发射/重命名周期,期间不能分配其他微操作。但对于low8/low16,合并微操作是“流程的一部分”,因此似乎不会导致额外的前端吞吐量惩罚,除了占据发射/重命名周期中的4个槽位之一。
在IvyBridge(或至少Haswell)中,英特尔放弃了对low8和low16寄存器的部分寄存器重命名,只保留了对high8寄存器(AH/BH/CH/DH)的重命名。读取high8寄存器会增加额外的延迟。此外,
setcc al
与Nehalem及之前的版本(可能包括Sandybridge)相比,在rax的旧值上存在虚假依赖。有关详细信息,请参阅
this HSW/SKL partial-register performance Q&A。
(我之前声称Haswell可以合并AH而不需要uop,但这是错误的,也不是Agner Fog的指南所说的。我浏览得太快了,不幸地在许多评论和其他帖子中重复了我的错误理解。)
AMD CPU和Intel Silvermont不会对部分寄存器(除了标志位)进行重命名,因此
mov al, [mem]
在eax的旧值上存在虚假依赖。(好处是当稍后读取完整寄存器时,没有部分寄存器合并的减速现象。)
通常情况下,只有在您的代码实际上依赖于
inc
不会触碰CF行为时,使用
add
而不是
inc
才能使您的代码在AMD或主流Intel上运行更快。也就是说,通常情况下,
add
只有在它会破坏您的代码时才有帮助,但请注意上面提到的
shl
情况,其中指令读取标志位,但通常您的代码并不关心这一点,因此这是一个虚假的依赖关系。
如果您确实希望保持CF不变,那么SnB系列之前的CPU在部分标志位停顿方面存在严重问题,但在SnB系列上,CPU合并部分标志位的开销非常低,因此在针对这些CPU时,最好继续使用
inc
或
dec
作为循环条件的一部分,并进行一定程度的展开。(有关详细信息,请参阅我之前提供的BigInteger
adc
问答链接)。如果您不需要根据结果进行分支,可以使用
lea
进行算术运算而不影响标志位,这可能会很有用。
Skylake没有部分标志合并成本
更新:Skylake根本没有部分标志合并uops:CF只是与FLAGS的其余部分分开的一个单独的寄存器。需要同时使用两个部分的指令(如cmovbe)会分别读取两个输入。这使得cmovbe成为一个2-uop指令,但大多数其他cmovcc指令在Skylake上是1-uop。参见
什么是部分标志停顿?。
adc仅仅读取CF,因此在Skylake上可以是单一uop,在同一个循环中与inc或dec没有任何交互。
(待办事项:重写答案的前面部分。)
inc/dec
对于旧值EFLAGS
没有假依赖。只是优化手册还没有更新。 - Peter Cordes