我正在使用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


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

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 ecxjnz .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.ANYRESOURCE_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的周期以查看完整模式。

10-08 01:37