为什么一个循环迭代中的依赖不能和上一个一起执行
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
由于dec
和jnz
将被宏融合为一个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 的指令级并行性,以保持 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-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 上,只有 popcnt
和 lzcnt/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。
我使用这段代码来测试 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
由于dec
和jnz
将被宏融合为一个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 的指令级并行性,以保持 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-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 上,只有 popcnt
和 lzcnt/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。