B 和 A 形成一个循环携带的依赖链。 下一次迭代中的A不能运行,直到它有上一次B的结果。
任何给定的B都不能在与A相同的循环中运行:如果较早的还没有产生结果,那么后面的将使用什么输入?
该链长 2 个周期(每次迭代),因为 的延迟inc
为 1 个周期。这会在后端造成无序执行无法隐藏的延迟瓶颈。(除了非常低的迭代次数,它可以在循环后与代码重叠)。
就像你完全展开一个巨大的链一样times 102400 inc eax
,CPU 无法在每条指令链之间找到指令级并行性,而每条指令链都依赖于前一条。
宏融合dec rcx/jnz
uop 独立于 RAX 链,是一个较短的链(每次迭代只有 1 个周期,只有 1 个 dec&branch uop,延迟为 1c)。因此它可以与 B 或 A 微指令并行运行。
有关指令级并行性和依赖链的概念以及 CPU 如何利用这种并行性在独立时并行运行指令的更多信息,请参阅我对另一个问题的回答。
Agner Fog 的 microarch PDF 在早期章节中通过示例展示了这一点:第 2 章:乱序执行(除 P1、PMMX 之外的所有处理器)。
如果每次迭代都启动一个新的 2 周期 dep 链,它会按预期运行。每次迭代都分叉的新链将为 CPU 暴露指令级并行性,以防止A和B同时进行不同的迭代。
.for_loop:
xor eax,eax ; dependency-breaking for RAX
inc rax ; uop A
inc rax ; uop B
dec rcx ; uop C
jnz .for_loop
Sandybridge 系列在没有执行单元的情况下处理异或归零,因此在循环中这仍然只有 3 个未融合域 uop,因此 IvyBridge 有足够的 ALU 执行端口来在一个周期内运行所有 3 个。这也以每个时钟 4 个融合域微指令最大化了前端。
或者,如果您更改 A 以在 RAX 中启动一个新的 dep 链,并使用任何无条件覆盖 RAX 的指令而不依赖inc
.
lea rax, [rdx + rdx] ; no dependency on B from last iter
inc rax ; uop B
除了一对不幸的输出依赖的指令:为什么打破 LZCNT 的“输出依赖”很重要?
popcnt rax, rdx ; false dependency on RAX, 3 cycle latency
inc rax ; uop B
在 Intel CPU 上,只有popcnt
和lzcnt/tzcnt
无缘无故地具有输出依赖性。bsf
这是因为它们使用与/相同的执行单元bsr
,如果输入为零,则在 Intel 和 AMD CPU 上保持目标不变。如果BSF / BSR的输入为零,英特尔仍然仅在纸上将其记录为未定义,但他们构建了实现更强保证的硬件。(AMD 甚至确实记录了这种 BSF/BSR 行为。)无论如何,英特尔的 BSF/BSR 就像 CMOV,并且需要目标作为输入,以防源 reg 为 0。 popcnt
(以及前 Skylake 上的 lzcnt/tzcnt)受苦也由此而来。
如果您使循环超过 5 个融合域微指令,则 SnB/IvB 最多可以从前端每 2 个循环发出 1 个。Haswell 和后来在循环缓冲区中“展开”或其他东西,因此 5 uop 循环每次迭代可以以 ~1.25 c 运行,但 SnB/IvB 不能。 执行 uop 计数不是处理器宽度倍数的循环时性能会降低吗?
自 Core 2 以来,Intel CPU 的前端问题/重命名阶段为 4 个融合域 uops 宽。