我正在使用this answer中的代码,对其进行了一些修改:
BITS 64
GLOBAL _start
SECTION .text
_start:
mov ecx, 1000000
.loop:
;T is a symbol defined with the CLI (-DT=...)
TIMES T imul eax, eax
lfence
TIMES T imul edx, edx
dec ecx
jnz .loop
mov eax, 60 ;sys_exit
xor edi, edi
syscall
没有
lfence
,我得到的结果与该答案中的静态分析一致。当我引入单个
lfence
时,我希望CPU与下一个(k + 1)迭代的imul edx, edx
序列并行执行第k个迭代的imul eax, eax
序列。这样的事情(将A称为
imul eax, eax
序列,将D称为imul edx, edx
序列):|
| A
| D A
| D A
| D A
| ...
| D A
| D
|
V time
花费或多或少相同的周期数,但要执行一个不成对的并行执行。
当我测量周期数时,对于原始版本和修改版本,对于
taskset -c 2 ocperf.py stat -r 5 -e cycles:u '-x ' ./main-$T
在以下范围内,使用T
T Cycles:u Cycles:u Delta
lfence no lfence
10 42047564 30039060 12008504
15 58561018 45058832 13502186
20 75096403 60078056 15018347
25 91397069 75116661 16280408
30 108032041 90103844 17928197
35 124663013 105155678 19507335
40 140145764 120146110 19999654
45 156721111 135158434 21562677
50 172001996 150181473 21820523
55 191229173 165196260 26032913
60 221881438 180170249 41711189
65 250983063 195306576 55676487
70 281102683 210255704 70846979
75 312319626 225314892 87004734
80 339836648 240320162 99516486
85 372344426 255358484 116985942
90 401630332 270320076 131310256
95 431465386 285955731 145509655
100 460786274 305050719 155735555
Cycles:u lfence
的值如何解释?我希望它们与
Cycles:u no lfence
相似,因为单个lfence
应该只防止对两个块并行执行第一次迭代。我不认为这是由于
lfence
开销引起的,因为我认为对于所有T
来说,这应该是恒定的。在处理静态代码分析时,我想解决格式问题。
Supporting repository with source files。
最佳答案
我将针对两种代码(带和不带lfence
)的T = 1的情况进行分析。然后,可以将其扩展为T的其他值。可以参考《英特尔优化手册》的图2.4。
因为只有一个容易预测的分支,所以只有在后端停止时,前端才会停止。前端在Haswell中为4宽,这意味着最多可以从IDQ(指令解码队列,它只是保存有序融合域uops的队列,也称为uop队列)发出4个融合uops。预订站(RS)整个调度程序。每个imul
都被解码为无法融合的单个uop。指令dec ecx
和jnz .loop
在前端被宏融合为单个uop。微融合和宏融合之间的区别之一是,当调度程序将宏融合的uop(不是微融合的)调度到分配给它的执行单元时,它将作为单个uop进行调度。相比之下,需要将微融合的uop拆分为其组成的uop,每个uop必须分别分配给执行单元。 (但是,分裂的微融合物会在RS的入口处发生,而不是在调度时发生,请参阅@Peter答案中的脚注2)。 lfence
解码为6微码。识别微融合仅在后端很重要,在这种情况下,回路中没有微融合。
由于循环分支很容易预测,并且迭代次数相对较大,因此我们可以假设在不影响准确性的前提下,分配器将始终能够在每个周期分配4微码。换句话说,调度程序每个周期将收到4 uops。由于没有混乱,每个小伙子都将作为单个小伙子分发。imul
只能由Slow Int执行单元执行(请参见图2.4)。这意味着执行imul
uops的唯一选择是将它们分派到端口1。在Haswell中,Slow Int的流水线很好,因此每个周期可以分派一个imul
。但是它需要三个周期才能使乘法结果可用于任何需要的指令(写回阶段是管道调度阶段的第三个周期)。因此,对于每个依赖链,每3个周期最多可以调度一个imul
。
因为dec/jnz
是预计采用的,所以唯一可以执行它的执行单元是端口6上的Primary Branch。
因此,在任何给定的周期内,只要RS有空间,它将收到4 uops。但是什么样的?让我们不加思索地检查循环:
imul eax, eax
imul edx, edx
dec ecx/jnz .loop (macrofused)
有两种可能性:
同一迭代中的两个
imul
,相邻迭代中的一个imul
,以及这两个迭代之一中的一个dec/jnz
。一个迭代中的一个
dec/jnz
,下一个迭代中的两个imul
,以及同一迭代中的一个dec/jnz
。因此,在任何周期的开始,RS将从每个链接收至少一个
dec/jnz
和至少一个imul
。同时,在相同的周期中,从RS中已经存在的uops中,调度程序将执行以下两个操作之一:将最旧的
dec/jnz
分发到端口6,然后将最旧的imul
分发到端口1。总共2块。由于Slow Int的延迟为3个周期,但是只有两条链,因此对于3个周期的每个周期,RS中没有
imul
准备执行。但是,RS中始终至少有一个dec/jnz
。因此,调度程序可以调度它。总计1 uop。现在,我们可以在任何给定周期N结束时计算RS XN中的预期微指令数:
XN = XN-1 +(要在周期N的开始处分配给RS的微指令数)-(在周期N的开始要分配的预期微码数)
= XN-1 + 4-((0 + 1)* 1/3 +(1 + 1)* 2/3)
= XN-1 + 12/3-5/3
= XN-1 +所有N> 0的7/3
重复的初始条件为X0 =4。这是一个简单的重复,可以通过展开XN-1来解决。
XN = 4 + 2.3 * N全部为N> = 0
Haswell的RS有60个条目。我们可以确定期望RS满负荷的第一个周期:
60 = 4 + 7/3 * N
N = 56 / 2.3 = 24.3
因此,在周期24.3结束时,预计RS将已满。这意味着在周期25.3的开始,RS将无法接收任何新的指令。现在,正在考虑的迭代次数I确定了如何进行分析。由于依赖关系链将需要至少3 * I的周期才能执行,因此大约需要进行8.1次迭代才能达到周期24.3。因此,如果迭代次数大于8.1(在这里就是这种情况),则需要分析在周期24.3之后发生的情况。
调度程序在每个周期以以下速率调度指令(如上所述):
1
2
2
1
2
2
1
2
.
.
但是,除非至少有4个可用条目,否则分配器将不会在RS中分配任何微指令。否则,它不会以次优的吞吐量浪费发行uops的功率。但是,仅在每个第4个周期的开始,RS中至少有4个空闲条目。因此,从周期24.3开始,分配器将每4个周期中的3个停顿。
对于正在分析的代码的另一个重要观察结果是,永远不会发生超过4个uop的调度,这意味着每个周期离开其执行单元的uop的平均数量不大于4。可以从重新排序缓冲区(ROB)中退出。这意味着ROB永远不会处于关键路径上。换句话说,性能由调度吞吐量决定。
现在,我们可以很容易地计算出IPC(每个周期的指令)。 ROB条目如下所示:
imul eax, eax - N
imul edx, edx - N + 1
dec ecx/jnz .loop - M
imul eax, eax - N + 3
imul edx, edx - N + 4
dec ecx/jnz .loop - M + 1
右边的列显示了可以撤消指令的周期。退休是按顺序进行的,并受关键路径的延迟的限制。在这里,每个依赖链都具有相同的路径长度,因此都构成了两个相等的长度为3个周期的关键路径。因此,每3个周期可以取消4条指令。因此IPC为4/3 = 1.3,而CPI为3/4 = 0.75。这比理论上的最佳IPC 4小得多(即使不考虑微观和宏观融合)。因为退休是按顺序发生的,所以退休行为将是相同的。
我们可以使用
perf
和IACA来检查分析。我将讨论perf
。我有一个Haswell CPU。perf stat -r 10 -e cycles:u,instructions:u,cpu/event=0xA2,umask=0x10,name=RESOURCE_STALLS.ROB/u,cpu/event=0x0E,umask=0x1,cmask=1,inv=1,name=UOPS_ISSUED.ANY/u,cpu/event=0xA2,umask=0x4,name=RESOURCE_STALLS.RS/u ./main-1-nolfence
Performance counter stats for './main-1-nolfence' (10 runs):
30,01,556 cycles:u ( +- 0.00% )
40,00,005 instructions:u # 1.33 insns per cycle ( +- 0.00% )
0 RESOURCE_STALLS.ROB
23,42,246 UOPS_ISSUED.ANY ( +- 0.26% )
22,49,892 RESOURCE_STALLS.RS ( +- 0.00% )
0.001061681 seconds time elapsed ( +- 0.48% )
有100万次迭代,每个迭代大约需要3个周期。每个迭代包含4条指令,IPC为1.33。
RESOURCE_STALLS.ROB
显示由于ROB满而使分配器停止的周期数。当然,这永远不会发生。 UOPS_ISSUED.ANY
可用于计算发给RS的uops数量以及分配器停止的周期数(无特定原因)。第一个很简单(在perf
输出中未显示);第二个很简单。 1百万* 3 = 3百万+小噪音。后者更有趣。它表明,由于RS满,分配器在所有时间中停顿了大约73%,这与我们的分析相符。 RESOURCE_STALLS.RS
计算由于RS满而使分配器停止的周期数。这接近于UOPS_ISSUED.ANY
,因为分配器不会由于任何其他原因而停顿(尽管由于某种原因该差异可能与迭代次数成正比,但我必须查看T> 1的结果)。可以扩展对不带
lfence
的代码的分析,以确定如果在两个lfence
之间添加imul
会发生什么。让我们先检查perf
结果(很遗憾,IACA不支持lfence
):perf stat -r 10 -e cycles:u,instructions:u,cpu/event=0xA2,umask=0x10,name=RESOURCE_STALLS.ROB/u,cpu/event=0x0E,umask=0x1,cmask=1,inv=1,name=UOPS_ISSUED.ANY/u,cpu/event=0xA2,umask=0x4,name=RESOURCE_STALLS.RS/u ./main-1-lfence
Performance counter stats for './main-1-lfence' (10 runs):
1,32,55,451 cycles:u ( +- 0.01% )
50,00,007 instructions:u # 0.38 insns per cycle ( +- 0.00% )
0 RESOURCE_STALLS.ROB
1,03,84,640 UOPS_ISSUED.ANY ( +- 0.04% )
0 RESOURCE_STALLS.RS
0.004163500 seconds time elapsed ( +- 0.41% )
观察到周期数增加了约1000万,即每次迭代增加了10个周期。周期数并不能告诉我们太多。退休指令的数量已增加了一百万,这是预期的。我们已经知道
lfence
不会使指令更快地完成,因此RESOURCE_STALLS.ROB
不应更改。 UOPS_ISSUED.ANY
和RESOURCE_STALLS.RS
特别有趣。在此输出中,UOPS_ISSUED.ANY
计算周期,而不是计数。还可以计算uops的数量(使用cpu/event=0x0E,umask=0x1,name=UOPS_ISSUED.ANY/u
代替cpu/event=0x0E,umask=0x1,cmask=1,inv=1,name=UOPS_ISSUED.ANY/u
),并且每次迭代增加了6 oups(无融合)。这意味着放置在两个lfence
之间的imul
被解码为6微码。现在,一百万美元的问题是这些微件会做什么以及它们如何在管道中移动。RESOURCE_STALLS.RS
为零。那是什么意思?这表明当分配器在IDQ中看到lfence
时,它将停止分配,直到ROB中的所有当前微指令都退出为止。换句话说,分配器不会在lfence
退休之前在RS中分配lfence
之后的条目。由于循环主体仅包含其他3个uoop,因此60项RS将永远不会满。实际上,它几乎总是空的。实际上,IDQ并不是一个简单的队列。它由可以并行操作的多个硬件结构组成。
lfence
所需的微指令数取决于IDQ的确切设计。分配器也由许多不同的硬件结构组成,当它看到IDQ的任何结构的前面都有lfence
uops时,它将暂停从该结构进行分配,直到ROB为空。因此,使用不同的硬件结构来使用不同的指令。UOPS_ISSUED.ANY
显示分配器在每次迭代大约9-10个周期内不发出任何uop。这是怎么回事好吧,lfence
的用途之一是它可以告诉我们退休指令和分配下一条指令需要花费多少时间。以下汇编代码可用于执行此操作:TIMES T lfence
对于
T
的较小值,性能事件计数器将无法正常工作。对于足够大的T,通过测量UOPS_ISSUED.ANY
,我们可以确定退出每个lfence
大约需要4个周期。这是因为UOPS_ISSUED.ANY
每5个周期将增加约4倍。因此,每隔4个周期,分配器就会发出另一个lfence
(它不会停止),然后等待另外4个周期,依此类推。也就是说,根据指令的不同,产生结果的指令可能需要1个或几个周期才能退出。 IACA始终假定退休指令需要5个周期。我们的循环如下所示:
imul eax, eax
lfence
imul edx, edx
dec ecx
jnz .loop
在
lfence
边界的任何周期,ROB将从ROB的顶部开始包含以下指令(最早的指令):imul edx, edx - N
dec ecx/jnz .loop - N
imul eax, eax - N+1
其中N表示分配相应指令的周期数。将要完成(到达写回阶段)的最后一条指令是
imul eax, eax
。这发生在周期N + 4。分配器停顿周期数将在N + 1,N + 2,N + 3和N + 4周期内增加。但是,直到imul eax, eax
退出之前,大约还有5个周期。另外,分配器退出后,需要清除IDQ中的lfence
指令并分配下一组指令,然后才能在下一个周期中分派它们。 perf
输出告诉我们,每次迭代大约需要13个周期,并且在这13个周期中有10个分配器停滞(由于lfence
)。问题中的图表仅显示了直到T = 100的循环数。但是,此时还有另一个(最终)膝盖。因此,最好绘制T = 120的周期以查看完整模式。