为什么一个循环迭代中的依赖不能和上一个一起执行

Why dependency in a loop iteration can't be executed together with the previous one

我使用这段代码来测试 IvyBridge 循环迭代中依赖的影响:

global _start
_start:
    mov rcx,    1000000000
.for_loop:          
    inc rax     ; uop A
    inc rax     ; uop B
    dec rcx     ; uop C
    jnz .for_loop   

    xor rdi,    rdi
    mov rax,    60  ; _exit(0)
    syscall

由于decjnz将被宏融合为一个uop,我的循环中有3个uop,它们在评论中被标记。

uop B 依赖于 uop A,所以我认为执行是这样的:

A C
B A C  ; the previous B and current A can be in the same cycle
B A C
...
B A C
B

因此循环可以每个 iter 执行 1 个周期。

但是,perf 工具显示:

 2,009,704,779      cycles                
 1,008,054,984      stalled-cycles-frontend   #   50.16% frontend cycles idl

所以每个迭代器有 2 个周期,并且有 50% 的前端周期空闲。

什么导致前端 50% 空闲?为什么假设的执行图无法实现?

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 uops 并行。


请参阅 my answer on another question 了解更多关于 指令级并行性 和依赖链的概念,以及 CPU 如何利用该并行性 运行 并行指令 当它们独立时

Agner Fog's microarch PDF 在早期章节中通过示例展示了这一点:第 2 章:乱序执行(除 P1 之外的所有处理器, PMMX).


如果您每次迭代都启动一个新的 2-cycle dep 链,它将 运行 正如您所期望的那样 。每次迭代分叉的新链将暴露 CPU 的指令级并行性,以保持 AB 不受飞行中不同迭代的影响同时

.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-family 在没有执行单元的情况下处理异或归零,因此循环中仍然只有 3 个未融合域 uops,因此 IvyBridge 有足够的 ALU 执行端口 运行 在一个周期中全部 3 个.这也使前端达到每个时钟 4 个融合域微指令。

或者,如果您将 A 更改为使用任何无条件覆盖 RAX 的指令在 RAX 中启动一个新的 dep 链,而不依赖于 inc 的结果,您会没事的。

    lea  rax, [rdx + rdx]     ; no dependency on B from last iter
    inc  rax                  ; uop B

除了一些具有不幸的输出依赖性的指令:Why does breaking the "output dependency" of LZCNT matter?

    popcnt rax, rdx        ; false dependency on RAX, 3 cycle latency
    inc  rax               ; uop B

在 Intel CPUs 上,只有 popcntlzcnt/tzcnt 无缘无故地具有输出依赖性。这是因为它们使用与 bsf/bsr 相同的执行单元,在 Intel 和 AMD CPU 上,如果输入为零,目标将保持不变。如果 BSF/BSR 的输入为零,英特尔仍然只在纸上将其记录为未定义,但他们构建的硬件实现了更强的保证。 (AMD 甚至记录了这种 BSF/BSR 行为。)无论如何,英特尔的 BSF/BSR 就像 CMOV,并且需要目标作为输入以防源 reg 为 0。popcnt,(和lzcnt/tzcnt 在 Skylake 之前)也有这个问题。


如果您使循环超过 5 个融合域微指令,SnB/IvB 最多可以从前端每 2 个周期发出 1 个。 Haswell 和后来在循环缓冲区或其他东西中“展开”,因此 5 uop 循环可以 运行 每次迭代约 1.25 c,但 SnB/IvB 不会。

前端 issue/rename 阶段在 Intel CPUs 中自 Core 2 以来是 4 个融合域 uops。