7

首先,我在 IvyBridge 上进行了以下设置,我将在注释位置插入测量有效负载代码。前 8 个字节buf存储buf自身的地址,我用它来创建循环携带的依赖:

section .bss
align   64
buf:    resb    64

section .text
global _start
_start:
    mov rcx,         1000000000
    mov qword [buf], buf
    mov rax,         buf
loop:
    ; I will insert payload here
    ; as is described below 

    dec rcx
    jne loop

    xor rdi,    rdi
    mov rax,    60
    syscall

情况1:

我插入有效载荷位置:

mov qword [rax+8],  8
mov rax,            [rax]

perf显示循环为 5.4c/iter。这有点容易理解,因为 L1d 延迟是 4 个周期。

案例2:

我颠倒了这两条指令的顺序:

mov rax,            [rax]
mov qword [rax+8],  8

结果突然变成9c/iter。我不明白为什么。因为下一次迭代的第一条指令不依赖于当前迭代的第二条指令,所以这个设置不应该与案例 1 不同。

我也用 IACA 工具对这两种情况进行了静态分析,但该工具不可靠,因为它对两种情况的预测结果相同 5.71c/iter,这与实验相矛盾。

案例3:

然后我在案例 2 中插入一条不相关的mov指令:

mov rax,            [rax]
mov qword [rax+8],  8
mov rbx,            [rax+16] 

现在结果变成了 6.8c/iter。但是不相关的插入如何mov将速度从 9c/iter 提高到 6.8c/iter?

IACA 工具预测错误的结果,如前一种情况,它显示 5.24c/iter。

我现在完全糊涂了,如何理解上述结果?

编辑以获取更多信息:

在情况 1 和 2 中,有一个 address rax+8rax+8如果更改为rax+16或,则情况 1 和 2 的结果相同rax+24。但是当它改为rax+32:case 1 变成 5.3c/iter,case 2 突然变成 4.2c/iter。

编辑更多perf活动:

$ perf stat -ecycles,ld_blocks_partial.address_alias,int_misc.recovery_cycles,machine_clears.count,uops_executed.stall_cycles,resource_stalls.any ./a.out

案例1 [rax+8]

 5,429,070,287      cycles                                                        (66.53%)
         6,941      ld_blocks_partial.address_alias                                     (66.75%)
       426,528      int_misc.recovery_cycles                                      (66.83%)
        17,117      machine_clears.count                                          (66.84%)
 2,182,476,446      uops_executed.stall_cycles                                     (66.63%)
 4,386,210,668      resource_stalls.any                                           (66.41%)

案例2 [rax+8]

 9,018,343,290      cycles                                                        (66.59%)
         8,266      ld_blocks_partial.address_alias                                     (66.73%)
       377,824      int_misc.recovery_cycles                                      (66.76%)
        10,159      machine_clears.count                                          (66.76%)
 7,010,861,225      uops_executed.stall_cycles                                     (66.65%)
 7,993,995,420      resource_stalls.any                                           (66.51%)

案例3 [rax+8]

 6,810,946,768      cycles                                                        (66.69%)
         1,641      ld_blocks_partial.address_alias                                     (66.73%)
       223,062      int_misc.recovery_cycles                                      (66.73%)
         7,349      machine_clears.count                                          (66.74%)
 3,618,236,557      uops_executed.stall_cycles                                     (66.58%)
 5,777,653,144      resource_stalls.any                                           (66.53%)

案例2 [rax+32]

 4,202,233,246      cycles                                                        (66.68%)
         2,969      ld_blocks_partial.address_alias                                     (66.68%)
       149,308      int_misc.recovery_cycles                                      (66.68%)
         4,522      machine_clears.count                                          (66.68%)
 1,202,497,606      uops_executed.stall_cycles                                     (66.64%)
 3,179,044,737      resource_stalls.any                                           (66.64%)
4

1 回答 1

2

Tl;DR:对于这三种情况,当同时执行加载和存储时会产生几个周期的惩罚。在所有三种情况下,负载延迟都在关键路径上,但不同情况下的惩罚是不同的。由于附加负载,案例 3 大约比案例 1 高一个周期。


分析方法一:使用停顿性能事件

我能够在 IvB 和 SnB 的所有三个案例中重现您的结果。我得到的数字在你数字的 2% 以内。执行案例 1、2 和 4 的单次迭代所需的周期数分别为 5.4、8.9 和 6.6。

让我们从前端开始。和性能事件表明,基本上所有的微指令都是从 LSD 发出的LSD.CYCLES_4_UOPSLSD.CYCLES_3_UOPS此外,这些事件连同LSD.CYCLES_ACTIVE表明,在 LSD 没有停止的每个周期中,在情况 1 和 2 中发出 3 个 uops,在情况 3 中发出 4 个 uops。换句话说,正如预期的那样,每次迭代的 uops在一个周期内在同一组中一起发出。

在以下所有关系中,“=~”符号表示差异在 2% 以内。我将从以下经验观察开始:

UOPS_ISSUED.STALL_CYCLES+ LSD.CYCLES_ACTIVE=~cycles

请注意,SnB 上的 LSD 事件计数需要按照此处的讨论进行调整。

我们还有以下关系:

案例 1:UOPS_ISSUED.STALL_CYCLES=~ RESOURCE_STALLS.ANY=~ 4.4c/iter
案例 2:UOPS_ISSUED.STALL_CYCLES=~ RESOURCE_STALLS.ANY=~ 7.9c/iter
案例 3:UOPS_ISSUED.STALL_CYCLES=~ RESOURCE_STALLS.ANY=~ 5.6c/iter

这意味着问题停滞的原因是后端中的一个或多个所需资源不可用。因此,我们可以放心地从考虑中排除整个前端。在情况 1 和 2 中,该资源是 RS。在情况 3 中,由于 RS 造成的停顿约占所有资源停顿1的 20% 。

现在让我们关注案例 1。总共有 4 个未融合域 uop:1 个加载 uop、1 个 STA、1 个 STD 和 1 个 dec/jne。加载和 STA 微指令取决于先前的加载微指令。每当 LSD 发出一组微指令时,STD 和跳转微指令都可以在下一个周期被调度,因此下一个周期不会导致执行停顿事件。但是,可以调度加载和 STA 微指令的最早点是在写回加载结果的同一周期中。CYCLES_NO_EXECUTE和之间的相关性STALLS_LDM_PENDING表示没有准备好执行的微指令的原因是因为 RS 中的所有微指令都在等待 L1 为待处理的加载请求提供服务。具体来说,RS中的微指令有一半是加载微指令,另一半是STA,它们都在等待各自上一次迭代的加载完成。LSD.CYCLES_3_UOPS表明 LSD 一直等到 RS 中至少有 4 个空闲条目,然后才发出一组构成完整迭代的微指令。在下一个周期中,这些微指令中的两个将被调度,从而释放 2 个 RS 条目2. 另一个将不得不等待他们依赖的负载完成。很可能加载按程序顺序完成。因此,LSD 一直等到 STA 和尚未执行的最旧迭代的加载微指令离开 RS。因此,UOPS_ISSUED.STALL_CYCLES+ 1 =~ 平均负载延迟3。我们可以得出结论,案例 1 的平均负载延迟为 5.4c。这大部分适用于案例 2,除了一个区别,我将很快解释。

由于每次迭代中的微指令形成一个依赖链,我们还有:

cycles=~平均负载延迟。

因此:

cycles=~ UOPS_ISSUED.STALL_CYCLES+ 1 =~ 平均加载延迟。

在案例 1 中,平均加载延迟为 5.4c。我们知道 L1 缓存的最佳情况延迟为 4c,因此负载延迟损失为 1.4c。但是为什么有效负载延迟不是 4c?

调度程序将预测 uops 所依赖的负载将在某个恒定延迟内完成,因此它将调度它们以进行相应的调度。如果由于任何原因(例如 L1 未命中)加载花费的时间超过该时间,则将调度 uops 但加载结果尚未到达。在这种情况下,微指令将被重放,并且发出的微指令的数量将大于发出的微指令的总数。

load 和 STA 微指令只能分派到端口 2 或 3。事件UOPS_EXECUTED_PORT.PORT_2UOPS_EXECUTED_PORT.PORT_3可用于分别统计分派到端口 2 和 3 的微指令数量。

案例 1:UOPS_EXECUTED_PORT.PORT_2+ UOPS_EXECUTED_PORT.PORT_3=~ 2uops/iter
案例 2:UOPS_EXECUTED_PORT.PORT_2+ UOPS_EXECUTED_PORT.PORT_3=~ 6uops/iter
案例 3:UOPS_EXECUTED_PORT.PORT_2+ UOPS_EXECUTED_PORT.PORT_3=~ 4.2uops/iter

情况1,AGU uops总调度数正好等于AGU uops退役数量;没有重播。所以调度器永远不会错误预测。在情况 2 中,每个 AGU uop 平均有 2 次重播,这意味着调度程序平均每个 AGU uop 错误预测两次。为什么在案例 2 中有错误预测,而在案例 1 中没有?

由于以下任何原因,调度程序将根据负载重放微指令:

  • L1 缓存未命中。
  • 记忆消歧误判。
  • 内存一致性违规。
  • L1 缓存命中,但存在 L1-L2 流量。
  • 虚拟页码预测错误。
  • 其他一些(未记录的)原因。

使用相应的性能事件可以明确排除前 5 个原因。Patrick Fay(英特尔)

最后是的,在加载和存储之间切换时有“几个”空闲周期。我被告知不要比“一些”更具体。
...
SNB 可以在同一周期读取和写入不同的银行。

我发现这些陈述,也许是故意的,有点模棱两可。第一条语句表明 L1 的加载和存储永远不会完全重叠。第二个建议只有在存在不同存储体的情况下才能在同一周期中执行加载和存储。尽管去不同的银行可能既不是必要条件也不是充分条件。但可以肯定的是,如果有并发的加载和存储请求,加载(和存储)可能会延迟一个或多个周期。这解释了案例 1 中负载延迟的平均 1.4c 损失。

case 1 和 case 2 是有区别的。 case 1 中,STA 和 load uop 依赖于同一个 load uop 是在同一个周期内一起发出的。另一方面,在情况 2 中,依赖于同一负载 uop 的 STA 和负载 uop 属于两个不同的问题组。每次迭代的问题停止时间基本上等于顺序执行一次加载和退出一个存储所花费的时间。可以使用 估计每个操作的贡献CYCLE_ACTIVITY.STALLS_LDM_PENDING。执行 STA uop 需要一个周期,因此存储可以在紧随 STA 被调度的那个周期中退出。

平均负载延迟为CYCLE_ACTIVITY.STALLS_LDM_PENDING+ 1 个周期(调度负载的周期)+ 1 个周期(调度跳转 uop 的周期)。我们需要添加 2 个周期,CYCLE_ACTIVITY.STALLS_LDM_PENDING因为在这些周期中没有执行停顿,但它们构成了总加载延迟的一小部分。这等于 6.8 + 2 = 8.8 个周期 =~ cycles

在前十几个(左右)迭代的执行过程中,每个循环都会在 RS 中分配一个跳转和 STD 微指令。这些将始终在发布周期之后的周期中被调度执行。在某个时刻,RS 将变满,所有尚未分派的条目将是 STA 和加载 uop,它们正在等待各自先前迭代的加载 uop 完成(写回它们的结果)。因此分配器将停止,直到有足够的空闲 RS 条目来发出整个迭代。让我们假设最旧的加载 uop 在周期T+ 0 处写回了它的结果。我将把加载 uop 所属的迭代称为当前迭代。将发生以下事件序列:

在cycle T+ 0:调度当前迭代的STA uop和下一次迭代的load uop。由于没有足够的 RS 条目,此循环中没有分配。此周期被计为分配停顿周期,但不计为执行停顿周期。

在周期T+ 1:STA uop 完成执行并且存储退出。分配要分配的下一次迭代的微指令。该周期被计为执行停顿周期,但不计为分配停顿周期。

在周期T+ 2:刚刚分配的跳转和 STD 微指令被调度。此周期被计为分配停顿周期,但不计为执行停顿周期。

在周期T+ 3 到T+ 3 + CYCLE_ACTIVITY.STALLS_LDM_PENDING- 2 处:所有这些周期都计为执行和分配停顿周期。请注意,这里有CYCLE_ACTIVITY.STALLS_LDM_PENDING- 1 个周期。

因此,UOPS_ISSUED.STALL_CYCLES应该等于 1 + 0 + 1 + CYCLE_ACTIVITY.STALLS_LDM_PENDING- 1。让我们检查一下:7.9 = 1+0+1+6.8-1。

按照案例 1 的推理,cycles应该等于UOPS_ISSUED.STALL_CYCLES+ 1 = 7.9 + 1 =~ 实际测量值cycles。同时执行加载和存储时产生的惩罚比情况 1 高 3.6c。就好像加载正在等待存储被提交。我认为这也解释了为什么在案例 2 中有重播但在案例 1 中没有。

情况 3,有 1 个 STD、1 个 STA、2 个负载和 1 个跳跃。单次迭代的微指令可以全部在一个周期内分配,因为 IDQ-RS 带宽是每个周期 4 个融合微指令。uops 在 RS 入口处未融合。1 个 STD 需要 1 个周期才能分派。跳跃也需要1个周期。有 3 个 AGU 微控制器,但只有 2 个 AGU 端口。因此,调度 AGU 微指令需要 2 个周期(与情况 1 和 2 中的 1 个周期相比)。派出的 AGU uops 组将是以下之一:

  • 第二次加载uop 和STA uop 相同的迭代。这些取决于同一迭代的第一个加载uop。两个 AGU 端口都被使用。
  • 下一次迭代的第一个加载 uop 可以在下一个循环中分派。这取决于前一次迭代的负载。仅使用两个 AGU 端口之一。

由于需要多一个周期才能释放足够多的 RS 条目以容纳整个问题组,UOPS_ISSUED.STALL_CYCLES+ 1 - 1 = UOPS_ISSUED.STALL_CYCLES=~ 平均负载延迟 =~ 5.6c,这与案例 1 非常接近。惩罚约为 1.6c . 这解释了为什么在案例 3 中与案例 1 和 2 相比,每个 AGU uop 平均被调度 1.4 次。

同样,因为释放足够的 RS 条目以容纳整个问题组需要更多的周期:

cycles=~ 平均负载延迟 + 1 = 6.6c/iter,实际上与cycles我的系统上测量的完全匹配。

与案例 2 类似的详细分析也可以在案例 3 上进行。在情况 3 中,STA 的执行与第二次加载的延迟重叠。两种负载的延迟也大多重叠。

我不知道为什么不同情况下的处罚不同。我们需要知道 L1D 缓存是如何设计的。无论如何,我有足够的信心,在加载延迟(和存储延迟)上会有“几个空闲周期”的惩罚来发布这个答案。


脚注

(1) 另外 80% 的时间花在负载矩阵上。手册中几乎没有提到这种结构。它用于指定 uops 和加载 uops 之间的依赖关系。估计在 SnB 和 IvB 上有 32 个条目没有记录的性能事件可以专门计算 LM 上的停顿。所有记录的资源停顿事件都是零。在情况 3 中,每次迭代有 3 个 5 微指令取决于先前的负载,因此很可能 LM 将在任何其他结构之前被填充。在 IvB 和 SnB 上,RS 条目的“有效”数量估计分别约为 51 和 48。

(2) 我可能在这里做了一个无害的简化。请参阅即使 RS 未完全充满,RESOURCE_STALLS.RS 事件是否也可能发生?.

(3) 创建通过管道的 uop 流的可视化可能会有所帮助,以了解这一切如何组合在一起。您可以使用简单的负载链作为参考。这对于案例 1 来说很容易,但对于案例 2,由于重放,这很困难。


分析方法二:使用负载延迟性能监控工具

我想出了另一种分析代码的方法。这种方法更容易但不太准确。但是,它确实使我们得出了相同的结论。

另一种方法是基于MEM_TRANS_RETIRED.LOAD_LATENCY_*性能事件。这些事件在某种意义上是特殊的,因为它们只能在精确级别进行计数(请参阅:PERF STAT 不计算内存加载但计算内存存储)。

例如,MEM_TRANS_RETIRED.LOAD_LATENCY_GT_4计算延迟大于所有已执行负载的“随机”选择样本的 4 个核心周期的负载数量。延迟测量如下。第一次调度负载的周期是被视为负载延迟的一部分的第一个周期。写回加载结果的周期是被视为延迟一部分的最后一个周期。因此,重播被考虑在内。此外,从 SnB 开始(至少),根据此定义,所有负载的延迟都大于 4 个周期。当前支持的最小延迟阈值为 3 个周期。

Case 1
Lat Threshold  | Sample Count
 3             | 1426934
 4             | 1505684
 5             | 1439650
 6             | 1032657      << Drop 1
 7             |   47543      << Drop 2
 8             |   57681
 9             |   60803
10             |   76655
11             |     <10      << Drop 3

Case 2
Lat Threshold  | Sample Count
 3             | 1532028
 4             | 1536547
 5             | 1550828
 6             | 1541661
 7             | 1536371
 8             | 1537337
 9             | 1538440
10             | 1531577
11             |     <10      << Drop

Case 3
Lat Threshold  | Sample Count
 3             | 2936547
 4             | 2890162
 5             | 2921158
 6             | 2468704      << Drop 1
 7             | 1242425      << Drop 2
 8             | 1238254
 9             | 1249995
10             | 1240548
11             |     <10      << Drop 3

It's critical to understand that these numbers represents the number of loads of the randomly selected sample of all loads. For example, of the total size of the sample of all loads is 10 million and only 1 million of these has a latency larger than the specified threshold, then the measured value is 1 million. However, the total number of executed loads could be 1 billion. Therefore, the absolute values are not very meaningful themselves. What really matters is the pattern across different thresholds.

In case 1, there are three significant drops in the number of loads whose latency is larger than a specific threshold. We can deduce that loads whose latency is equal to or smaller than 6 cycles are the most common, loads whose latency is equal to or smaller than 7 cycles but larger than 6 cycles are the second most common, and most other loads have a latency between 8-11 cycles.

we already know that the minimum latency is 4 cycles. Given these numbers, it's reasonable to estimate the average load latency to be somewhere between 4 and 6 cycles, but closer to 6 than 4. We know from Method 1 that the average load latency is actually 5.4c. So we can make a fairly good estimation using these numbers.

In case 2, we can deduce that most loads have a latency that is smaller than or equal to 11 cycles. The average load latency is probably also much larger than 4, given the consistency in the measured number of loads across a wide range of latency thresholds. So it's between 4 and 11, but closer to 11 than 4. We know from Method 1 that the average load latency is actually 8.8c, which is close to any reasonable estimation based on these numbers.

Case 3 is similar to case 1 and in fact they actual average load latency determined using Method 1 is almost the same for these two cases.

Making measurements using MEM_TRANS_RETIRED.LOAD_LATENCY_* is easy and such analysis can be done by someone with little knowledge about the microarchitecture.

于 2019-01-20T22:58:24.823 回答