INC 指令与 ADD 1:重要吗?

INC instruction vs ADD 1: Does it matter?

来自Ira Baxter answer on, Why do the INC and DEC instructions not affect the Carry Flag (CF)?

Mostly, I stay away from INC and DEC now, because they do partial condition code updates, and this can cause funny stalls in the pipeline, and ADD/SUB don't. So where it doesn't matter (most places), I use ADD/SUB to avoid the stalls. I use INC/DEC only when keeping the code small matters, e.g., fitting in a cache line where the size of one or two instructions makes enough difference to matter. This is probably pointless nano[literally!]-optimization, but I'm pretty old-school in my coding habits.

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

根据指令的 CPU 实施,部分寄存器更新可能会导致停顿。根据Agner Fog's optimization guide, page 62,

For historical reasons, the INC and DEC instructions leave the carry flag unchanged, while the other arithmetic flags are written to. This causes a false dependence on the previous value of the flags and costs an extra μop. To avoid these problems, it is recommended that you always use ADD and SUB instead of INC and DEC. For example, INC EAX should be replaced by ADD EAX,1.

另请参阅 "Partial flags stalls" 的第 83 页和 "Partial flags stall" 的第 100 页。

更新:Efficiency cores on Alder Lake are Gracemont, and run inc reg as a single uop, but at only 1/clock, vs. 4/clock for add reg, 1 (https://uops.info/)。这可能是对 FLAGS 的错误依赖,就像 P4 那样; uops.info 测试没有尝试添加 dep-breaking 指令。除了 TL:DR,我还没有更新这个答案的其他部分。


TL:DR/advice for modern CPUs: Probably use add; Intel Alder Lake 的 E-cores 与“通用”调整相关并且似乎 运行 inc 缓慢 .

除了 Alder Lake 和更早的 Silvermont-family,使用 inc 除了内存目标;这对主流英特尔或任何 AMD 来说都很好。 (例如像 gcc -mtune=core2-mtune=haswell-mtune=znver1)。 inc mem 比 Intel P6 / SnB-family 上的 add 多花费 uop;负载不能micro-fuse.

如果您关心 Silvermont-family(包括 Xeon Phi 中的 KNL,以及一些上网本、Chromebook 和 NAS 服务器),可能会避免 incadd 1 在 64 位代码中仅花费 1 个额外字节,或在 32 位代码中花费 2 个额外字节。但这不是性能灾难(只是在本地使用了 1 个额外的 ALU 端口,不会产生错误的依赖关系或大停顿),所以如果你不关心 much 关于 SMont 那么不要担心它。

编写 CF 而不是不修改它可能对可能受益于 CF 的其他周围代码有用 dep-breaking,例如转变。见下文。

如果你想 inc/dec 不触及 任何 标志,lea eax, [rax+1] 运行s 高效且具有相同的 code-size作为 add eax, 1。 (虽然通常在比 add/inc 更少的可能执行端口上,所以当销毁 FLAGS 不是问题时 add/inc 更好。https://agner.org/optimize/


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

inc 保存 1 个字节(64 位模式),或 2 个字节(操作码 0x40..F inc r32/dec r32 32 位模式下的短格式,re-purposed 作为 x86-64 的 REX 前缀)。这使得总代码大小的百分比差异很小。这有助于 instruction-cache 命中率、iTLB 命中率和必须从磁盘加载的页面数。

inc的优点:

  • code-size直接
  • 不使用立即数会对 Sandybridge-family 产生 uop-cache 影响,这可能会抵消 add 更好的 micro-fusion。 (参见 Agner Fog's table 9.1 in the Sandybridge section of his microarch guide。)性能计数器可以很容易地测量 issue-stage 微指令,但很难衡量事物如何装入微指令缓存和 uop-cache 读取带宽影响。
  • 在某些情况下,不修改 CF 是一个优势,在 CPU 上,您可以在 inc 之后读取 CF 而不会出现停顿。 (不是在 Nehalem 和更早的版本上。)

现代 CPU 中有一个例外:Silvermont/Goldmont/Knight 的 Landing 有效解码 inc/dec 1 uop,但在 allocate/rename(又名问题)阶段扩展为 2。额外的 uop 合并部分标志。 inc throughput is only 1 per clock, vs. 0.5c (or 0.33c Goldmont) for independent add r32, imm8 因为由 flag-merging uops 创建的 dep 链。

与 P4 不同,寄存器结果在标志上没有 false-dep(见下文),因此 out-of-order 执行会在没有使用时将 flag-merging 关闭延迟关键路径标志结果。 (但 OOO window 比 Haswell 或 Ryzen 等主流 CPU 小得多。) 运行 inc 因为在大多数情况下,2 个独立的 uops 可能是 Silvermont 的胜利;大多数 x86 指令写入所有标志而不读取它们,从而打破了这些标志依赖链。

SMont/KNL 在解码和 allocate/rename 之间有一个队列(参见 Intel's optimization manual, figure 16-2),因此在发布期间扩展到 2 微指令可以填充解码停顿的气泡(在 [=401= 之类的指令上) ] mul,或 pshufb,它们从解码器中产生超过 1 个 uop,并导致微代码停顿 3-7 个周期)。或者在 Silvermont 上,仅包含 3 个以上前缀(包括转义字节和强制前缀)的指令,例如REX + 任何 SSSE3 或 SSE4 指令。但请注意,有一个 ~28 uop 循环缓冲区,因此小循环不会受到这些解码停顿的影响。

inc/dec 不是唯一解码为 1 但发出为 2 的指令:push/popcall/retlea 的 3 个组件也是这样做的。 KNL 的 AVX512 收集指令也是如此。资料来源:Intel's optimization manual、17.1.2 Out-of-Order 引擎 (KNL)。这只是一个小的吞吐量损失(如果其他任何东西是更大的瓶颈,有时甚至不是那个),所以通常仍然使用 inc 进行“通用”调整是好的。


Intel 的优化手册总体上仍然建议 add 1 而不是 inc,以避免 partial-flag 停顿的风险。但是由于英特尔的编译器默认情况下不会这样做,未来的 CPUs 不太可能使 inc 在所有情况下都变慢,就像 P4 那样。

Clang 5.0 and Intel's ICC 17 (on Godbolt) 在优化速度 (-O3) 时确实使用 inc,而不仅仅是大小。 -mtune=pentium4 让他们避免 inc/dec,但默认的 -mtune=generic 并没有给 P4 太多权重。

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


除了 Silvermont,这是 Pentium4 遗留下来的 mostly-stale 优化建议。在现代 CPUs 上,只有当您实际读取的标志不是由最后一个写入 any 标志的 insn 写入时才会出现问题。 (在这种情况下,您需要保留 CF,因此使用 add 会破坏您的代码。)

add 将所有 condition-flag 位写入 EFLAGS 寄存器。 Register-renaming 使 write-only 易于 out-of-order 执行:参见 write-after-write and write-after-read hazardsadd eax, 1add ecx, 1 可以并行执行,因为它们完全相互独立。 (甚至 Pentium4 也将条件标志位重命名为与 EFLAGS 的其余部分分开,因为即使 add 也没有修改 interrupts-enabled 和许多其他位。)

在 P4 上,incdec 取决于所有标志的先前值,因此它们不能与每个标志并行执行其他或之前的 flag-setting 说明。 (例如 add eax, [mem] / inc ecx 使 inc 等到 add 之后,即使添加的加载未命中缓存。)这称为错误依赖。 Partial-flag 通过读取标志的旧值来写入工作,更新 CF 以外的位,然后写入完整的标志。

所有其他 out-of-order x86 CPUs(包括 AMD 的),分别重命名标志的不同部分,所以在内部他们对所有的 write-only 更新除了 CF 之外的标志。 (来源:Agner Fog's microarchitecture guide)。只有少数指令,如 adccmc,真正读取然后写入标志。还有 shl r, cl(见下文)。


add dest, 1 优于 inc dest 的情况,至少对于英特尔 P6/SnB uarch 系列:

  • Memory-destination: add [rdi], 1 可以 micro-fuse the store and the load+add on Intel Core2 and SnB-family, 所以是 2 fused-domain uops / 4 unfused-domain哎呀。
    inc [rdi]只能micro-fuse店铺,所以是3F / 4U。
    根据 Agner Fog 的表格,AMD 和 Silvermont 运行 memory-dest incadd 相同,作为单个 macro-op / uop.

但要注意 uop-cache 对 add [label], 1 的影响,它需要一个 32 位地址和一个 8 位立即数用于相同的 uop。

在 Intel SnB-family 上,variable-count 移位是 3 微指令(从 Core2/Nehalem 上的 1 微指令上升)。 AFAICT,两个 uops read/write 标志,一个独立的 uop 读取 regcl,并写入 reg。这是一个奇怪的情况,延迟(1c + 不可避免的资源冲突)比吞吐量(1.5c)更好,并且只有在与打破标志依赖性的指令混合时才能实现最大吞吐量。 (I posted more about this 在 Agner Fog 的论坛上)。尽可能使用 BMI2 shlx;它是 1 uop,计数可以在任何寄存器中。

无论如何,inc(写入标志但保留 CF 未修改)在 variable-count shl 之前使其对最后写入的 CF 和 SnB/IvB 可能需要一个额外的 uop 来合并标志。

Core2/Nehalem 设法避免标志上的错误依赖:Merom 运行 是一个由 6 个独立 shl reg,cl 指令组成的循环,每个时钟几乎有两个班次,与 cl=0 的性能相同或 cl=13。每个时钟大于 1 的任何东西都证明标志上没有 input-dependency。

我尝试使用 shl edx, 2shl edx, 0 循环(immediate-count 转换),但没有看到 decsub 之间的速度差异Core2、HSW 或 SKL。我不知道 AMD。

更新:Intel P6 系列上出色的移位性能是以您需要避免的大型性能坑为代价的:当指令依赖于移位指令的 flag-result 时: 前端 会停止,直到指令 retired.(来源:Intel's optimization manual, (Section 3.5.2.6: Partial Flag Register Stalls))。所以 shr eax, 2 / jnz 对 Intel pre-Sandybridge 的性能来说是灾难性的,我猜!如果您关心 Nehalem 和更早版本,请使用 shr eax, 2 / test eax,eax / jnz。 Intel 的示例清楚地表明这适用于 immediate-count 班次,而不仅仅是 count=cl.

In processors based on Intel Core microarchitecture [this means Core 2 and later], shift immediate by 1 is handled by special hardware such that it does not experience partial flag stall.

Intel实际上是指没有立即数的特殊操作码,它通过隐式1移位。我认为编码 shr eax,1 的两种方式之间存在性能差异,短编码(使用原始 8086 操作码 D1 /5)产生 write-only (部分)标志结果,但是更长的编码(C1 /5, imm8 和立即数 1)直到执行时才检查其立即数是否为 0,但没有跟踪 out-of-order 机器中的标志输出。

由于遍历位很常见,但每隔 2 位(或任何其他步长)循环一次是非常不常见的,这看起来像是合理的设计选择。这解释了为什么编译器喜欢 test 移位的结果而不是直接使用来自 shr.

的标志结果

更新:对于 SnB-family 上的变量计数偏移,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.


术语

Partial-flag 停顿发生在读取标志时 ,如果它们发生的话。 P4 从来没有 partial-flag 个停顿,因为它们永远不需要合并。相反,它具有错误的依赖关系。

几个答案/评论混淆了术语。他们描述了一个错误的依赖关系,但随后称其为 partial-flag 停顿。这是因为只写了一些标志而发生的减速,但是术语“partial-flag stall”是 pre-SnB Intel 硬件上发生的事情 partial-flag 必须合并写入。 Intel SnB-family CPUs 插入一个额外的 uop 来合并标志而不停止。 Nehalem 和更早的停顿大约 7 个周期。我不确定对 AMD CPUs.

的惩罚有多大

(注意 partial-register 惩罚并不总是与 partial-flag 相同,见下文)。

### 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-family CPU 秒,.

在 P4 之后,Intel 基本上放弃了试图让人们使用 -mtune=pentium4 来 re-compile 或尽可能多地修改 hand-written asm 以避免严重的瓶颈。 (针对特定的微体系结构进行调优总是一件事情,但是 P4 在弃用这么多在以前的 CPUs 上很快的东西方面是不寻常的,因此在现有的二进制文件。)P4 希望人们使用 x86 的 RISC-like 子集,并且还具有 branch-prediction 提示作为 JCC 指令的前缀。 (它还有其他严重的问题,比如跟踪缓存不够好,以及解码器弱,这意味着 trace-cache 未命中时性能不佳。更不用说时钟非常高的整个哲学 运行进入 power-density 墙。)

当英特尔放弃 P4 (NetBurst uarch) 时,他们返回到 P6 系列设计 (Pentium-M / Core2 / Nehalem),继承了早期 P6 的 partial-flag / partial-reg 处理-family CPUs(PPro 到 PIII)pre-dated netburst mis-step。 (并不是所有关于 P4 的东西都天生就是坏的,Sandybridge 中的一些想法 re-appeared,但总体上 NetBurst 被广泛认为是一个错误。)一些 very-CISC 指令仍然比 multi-instruction 替代方案慢,例如enterloopbt [mem], reg(因为 reg 的值会影响使用哪个内存地址),但这些在旧的 CPU 中都很慢,因此编译器已经避免了它们.

Pentium-M 甚至改进了对 partial-reg 的硬件支持(更低的合并惩罚)。在 Sandybridge 中,Intel 保留了 partial-flag 和 partial-reg 重命名,并在需要合并时提高了效率(合并插入的 uop 没有或最小延迟)。 SnB 进行了重大的内部更改,被认为是一个新的 uarch 家族,尽管它从 Nehalem 继承了很多东西,并从 P4 继承了一些想法。 (但请注意,SnB 的 decoded-uop 缓存 不是 跟踪缓存,因此它是解决解码器 throughput/power 问题的一个非常不同的解决方案,NetBurst 的跟踪缓存试图解决求解。)


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

PPro/PIII 在读取完整的寄存器时停顿 5-6 个周期。 Core2/Nehalem 为部分 reg 插入合并 uop 时仅停顿 2 或 3 个周期,但部分标志仍然是更长的停顿。

SnB 插入合并 uop 而不会停止,就像标志一样。 Intel 的优化指南说,为了将 AH/BH/CH/DH 合并到更宽的 reg 中,插入合并 uop 需要整个 issue/rename 周期,在此期间不能分配其他 uop。但是对于 low8/low16,合并 uop 是“流程的一部分”,因此除了占用 issue/rename 循环中的 4 个槽之一之外,它显然不会导致额外的 front-end 吞吐量损失.

在 IvyBridge(或至少 Haswell)中,Intel 放弃了 partial-register 对 low8 和 low16 寄存器的重命名,仅保留对 high8 寄存器的重命名 (AH/BH/CH/DH)。读取 high8 寄存器有额外的延迟。此外,setcc al 对 rax 的旧值有错误的依赖性,这与 Nehalem 和更早版本(可能还有 Sandybridge)不同。详情见

(我之前声称 Haswell 可以在没有 uop 的情况下合并 AH,但这不是真的,也不是 Agner Fog 的指南所说的。我浏览得太快了,不幸的是在很多评论和其他帖子中重复了我的错误理解。 )

AMD CPUs 和 Intel Silvermont,不重命名部分 reg(标志除外),因此 mov al, [mem] 对 eax 的旧值有错误的依赖性。 (好处是没有 partial-reg 在稍后阅读完整的 reg 时合并减速。)


通常情况下,唯一一次 add 而不是 inc 会当您的代码实际上取决于 inc 的 t-touch-CF 行为时,您的代码在 AMD 或主流 Intel 上更快。即 通常 add 只有在它会破坏您的代码时才有帮助 ,但请注意上面提到的 shl 情况,指令读取标志但通常您的代码不会关心那个,所以这是一个错误的依赖。

如果你实际上想不修改 CF,SnB-familyCPUs 有严重的问题 partial-flag 停顿,但是SnB-family CPU 合并部分标志的开销非常低,因此最好在定位时继续使用 incdec 作为循环条件的一部分那些 CPU,有一些展开。 (有关详细信息,请参阅我之前链接的 BigInteger adc 问答)。如果您不需要对结果进行 b运行ch,那么使用 lea 进行算术运算而不影响标志可能很有用。


Skylake 没有 partial-flag 合并成本

更新:Skylake 根本没有 partial-flag 合并微指令:CF 只是一个独立于 FLAGS 其余部分的寄存器。需要这两个部分的指令(如 cmovbe)分别读取两个输入。这使得 cmovbe 成为 2-uop 指令,但大多数其他 cmovcc 指令在 Skylake 上都是 1-uop。参见 What is a Partial Flag Stall?

adc 读取 CF 因此它可以 single-uop 在 Skylake 上与 inc 或 [=36 完全没有交互=] 在同一个循环中。

(TODO:重写此答案的前面部分。)