了解带环对于具有两个长依赖性链的循环的影响,以增加长度

时间:2018-08-23 12:39:24

标签: performance assembly x86 cpu-architecture perf

我正在使用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次迭代的imul edx, edx序列下一个( k + 1th )迭代的imul eax, eax序列。
像这样的东西(分别叫{em> 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的{​​{1}}在下面的范围内

T

Plotted data of above

如何解释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来说,这应该是恒定的。

在处理静态代码分析时,我想解决 forma mentis 的问题。


Supporting repository with source files

1 个答案:

答案 0 :(得分:4)

我将针对两种代码(带和不带lfence)的T = 1的情况进行分析。然后,可以将其扩展为T的其他值。有关视觉效果,请参阅《英特尔优化手册》的图2.4。

因为只有一个容易预测的分支,所以只有在后端停止时,前端才会停止。前端在Haswell中为4宽,这意味着最多可以从IDQ(指令解码队列,它只是保存有序融合域uops的队列,也称为uop队列)发出4个融合uops。预订站(RS)整个调度程序。每个imul被解码成一个不能融合的uop。指令dec ecxjnz .loop在前端被宏融合为单个uop。微融合和宏融合之间的区别之一是,当调度程序将宏融合的uop(不是微融合的)调度到分配给它的执行单元时,它将作为单个uop进行调度。相比之下,需要将微融合的uop拆分为其组成的uop,每个uop必须分别分配给执行单元。 (但是,分裂的微融合物会在RS的入口处发生,而不是在调度时发生,请参见@Peter答案中的脚注2)。 lfence被解码为6微码。识别微融合仅在后端很重要,在这种情况下,循环中就没有微融合。

由于循环分支很容易预测,并且由于迭代次数相对较大,因此我们可以假设在不影响准确性的前提下,分配器将始终能够在每个周期分配4微秒。换句话说,调度程序每个周期将收到4 uops。由于没有混乱,因此每个uop都将作为一个uop进行调度。

imul只能由Slow Int执行单元执行(请参见图2.4)。这意味着执行imul uops的唯一选择是将它们调度到端口1。在Haswell中,Slow Int进行了很好的流水线处理,因此每个周期可以调度一个imul。但是它需要三个周期,乘法结果可用于任何需要的指令(写回阶段是管道调度阶段的第三个周期)。因此,对于每个依赖链,每3个周期最多可以调度一个imul

因为dec/jnz被预测为已采取,所以唯一可以执行的执行单元是端口6上的主分支。

因此,在任何给定的周期内,只要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 oups。
  • 由于Slow Int的延迟为3个周期,但是只有两条链,因此对于3个周期的每个周期,RS中没有imul可以执行。但是,RS中始终至少有一个dec/jnz。因此调度程序可以调度它。总共是1 uop。

现在,我们可以在任何给定周期N结束时计算RS中预期的uops数量X N

X N = X N-1 +(在循环N开始时要在RS中分配的uops数)-(预期的uops数将在第N周期开始时分派)
              = X N-1 + 4-((0 + 1)* 1/3 +(1 + 1)* 2/3)
              = X N-1 + 12/3-5/3
              =对于所有N> 0

,X N-1 + 7/3

重复的初始条件是X 0 =4。这是一个简单的重复,可以通过展开X N-1 来解决。

对于所有N> = 0的

X N = 4 + 2.3 * N

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)中退回4微码。这意味着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的结果)。 / p>

可以扩展对没有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.ANYRESOURCE_STALLS.RS特别有趣。在此输出中,UOPS_ISSUED.ANY计算周期,而不是计数。还可以计算微指令的数量(使用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微指令(无融合)。这意味着将放置在两个lfence之间的imul解码为6微码。现在,一百万美元的问题是这些微件会做什么以及它们如何在管道中移动。

RESOURCE_STALLS.RS为零。这意味着什么?这表明当分配器在IDQ中看到lfence时,它将停止分配,直到ROB中的所有当前微指令都退出为止。换句话说,直到lfence退休之前,分配器不会在lfence之后分配RS中的条目。由于循环主体仅包含其他3个uops,因此60项RS将永远不会满。实际上,它几乎总是空的。

实际上,IDQ并不是一个简单的队列。它由可以并行操作的多个硬件结构组成。 lfence所需的微指令数取决于IDQ的确切设计。分配器也由许多不同的硬件结构组成,当它看到IDQ的任何结构的前面有一个lfence微指令时,它会从该结构挂起分配,直到ROB为空。因此,不同的操作系统使用了不同的硬件结构。

UOPS_ISSUED.ANY表明分配器在每次迭代大约9-10个周期内不发出任何uops。这是怎么回事好吧,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的周期以查看完整模式。