首先,我在IvyBridge上进行了以下设置,我将在注释位置插入测量有效载荷代码。buf
的前 8 个字节存储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 中,有一个地址rax+8
。如果将情况 1 和 2 更改为rax+16
或rax+24
rax+8
,则情况 1 和 2 的结果保持不变。但是当它更改为rax+32
时,发生了一些令人惊讶的事情:案例 1 变为 5.3c/iter,案例 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%)
[rax+8]
的情况 2 :
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%)
tl;DR:对于这三种情况,同时执行加载和存储时会产生几个周期的损失。在所有三种情况下,负载延迟都在关键路径上,但在不同情况下的惩罚是不同的。由于额外的负载,情况 3 比情况 1 高出大约一个周期。
分析方法一:使用失速性能事件
我能够重现您在 IvB 和 SnB 上所有三个病例的结果。我得到的数字在你数字的 2% 以内。执行案例 1、2 和 4 的单次迭代所需的周期数分别为 5.4、8.9 和 6.6。
让我们从前端开始。LSD.CYCLES_4_UOPS
和LSD.CYCLES_3_UOPS
性能事件表明,基本上所有的uops都是从LSD发出的。此外,这些事件与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 uops 取决于以前的负载 uop。每当 LSD 发出一组 uops 时,STD 和跳跃 uops 都可以在下一个周期中调度,因此下一个周期不会导致执行停止事件。但是,可以调度负载和 STA uops 的最早点与回写加载结果的周期相同。CYCLES_NO_EXECUTE
和STALLS_LDM_PENDING
之间的相关性表明,没有准备执行的 uop 的原因是 RS 中的所有 uop 都在等待 L1 为挂起的加载请求提供服务。具体来说,RS 中的一半 uop 是加载 uop,另一半是 STA,它们都在等待各自先前迭代的加载完成。LSD.CYCLES_3_UOPS
表明 LSD 会等到 RS 中至少有 4 个空闲条目,然后才会发出一组构成完整迭代的 uop。在下一个周期中,将发送其中两个 uop,从而释放 2 个 RS 条目2。另一个将不得不等待他们所依赖的负载完成。加载很可能按程序顺序完成。因此,LSD 会等到 STA 并加载尚未执行的最旧迭代的 uops 离开 RS。因此,UOPS_ISSUED.STALL_CYCLES
+1=~平均加载延迟3。我们可以得出结论,情况 1 中的平均加载延迟为 5.4c。其中大部分适用于情况 2,除了一个区别,我将在稍后解释。
由于每次迭代中的 uops 形成一个依赖链,我们还有:
cycles
=~ 平均加载延迟。
因此:
cycles
=~UOPS_ISSUED.STALL_CYCLES
+ 1 =~ 平均加载延迟。
在情况 1 中,平均加载延迟为 5.4c。我们知道 L1 缓存的最佳情况下延迟是 4c,因此负载延迟损失为 1.4c。但是为什么有效负载延迟不是 4c?
调度程序将预测 uops 所依赖的负载将在某个恒定的延迟内完成,因此它将相应地调度它们。如果由于任何原因(例如 L1 未命中)加载花费的时间超过此时间,则将调度 uops,但加载结果尚未到达。在这种情况下,将重放 uop,并且已调度的 uop 数将大于已颁发的 uop 总数。
负载和 STA uops 只能调度到端口 2 或 3。事件UOPS_EXECUTED_PORT.PORT_2
和UOPS_EXECUTED_PORT.PORT_3
可用于分别计算调度到端口 2 和 3 的 uop 数。
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 uop 总数正好等于停用的 AGU uop 数;没有重播。因此,调度程序永远不会错误预测。在情况 2 中,每个 AGU uop 平均有 2 个重播,这意味着调度程序平均每个 AGU uop 错误预测两次。为什么在案例 2 中存在错误预测,而在案例 1 中却没有?
由于以下任何原因,调度程序将根据负载重播 uops:
- 一级缓存未命中。
- 内存消歧错误预测。
- 内存一致性冲突。 L1
- 缓存命中,但有 L1-L2 流量。
- 虚拟页码错误预测。
- 其他一些(未记录的)原因。
使用相应的性能事件可以明确排除前 5 个原因。帕特里克·费伊(英特尔)说:
最后是的,在 加载和存储。我被告知不要比"几个"更具体。
。
瑞士央行可以在同一周期内读写不同的银行。
我觉得这些说法,也许是故意的,有点模棱两可。第一个语句表明,对 L1 的加载和存储永远不会完全重叠。第二个建议,只有在存在不同银行的情况下,才能在同一周期内执行加载和存储。尽管去不同的银行可能既不是必要条件也不是充分条件。但有一件事是肯定的,如果有并发加载和存储请求,加载(和存储)可能会延迟一个或多个周期。这解释了情况 1 中负载延迟的平均 1.4c 损失。
案例1 和案例 2 之间存在差异。在情况 1 中,依赖于相同负载 uop 的 STA 和负载 uop 在同一周期中一起发出。另一方面,在情况 2 中,依赖于同一负载 uop 的 STA 和负载 uop 属于两个不同的问题组。每次迭代的问题停止时间基本上等于按顺序执行一个加载和停用一个存储所需的时间。可以使用CYCLE_ACTIVITY.STALLS_LDM_PENDING
估算每个操作的贡献。执行 STA uop 需要一个周期,因此商店可以在紧跟在调度 STA 的周期之后的周期中停用。
CYCLE_ACTIVITY.STALLS_LDM_PENDING
+ 1 个周期(调度负载的周期)+ 1 个周期(调度跳转 uop 的周期)。我们需要向CYCLE_ACTIVITY.STALLS_LDM_PENDING
添加 2 个周期,因为这些周期中没有执行停滞,但它们只占总负载延迟的一小部分。这等于 6.8 + 2 = 8.8 个周期 =~cycles
。
在执行前十几次(左右)迭代期间,每个周期都会在 RS 中分配一个跳转和 STD uops。这些将始终在问题周期之后的周期中调度执行。在某个时候,RS 将满,所有尚未调度的条目都将是 STA 并加载正在等待相应先前迭代的加载 uops 完成(写回其结果)。因此,分配器将停止,直到有足够的可用 RS 条目来发出整个迭代。假设最旧的加载 uop 在周期T
+ 0 处写回其结果。我将加载 uop 所属的迭代称为当前迭代。将发生以下事件序列:
在周期T
+ 0:调度当前迭代的 STA uop 和下一个迭代的加载 uop。此周期中没有分配,因为没有足够的 RS 条目。此周期计为分配停顿周期,但不计为执行停顿周期。
在周期T
+ 1 时:STA uop 完成执行,存储停用。分配要分配的下一个迭代的 uop。此周期计为执行停顿周期,但不计为分配停顿周期。
在周期T
+ 2 时:刚刚分配的跳转和 STD uops 被调度。此周期计为分配停顿周期,但不计为执行停顿周期。
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 个跳跃。单个迭代的 uops 都可以在一个周期内分配,因为 IDQ-RS 带宽为每个周期 4 个融合 uop。uops 在 RS 入口处未融合。1 STD 需要 1 个周期才能调度。跳跃也需要 1 个周期。有三个 AGU uop,但只有 2 个 AGU 端口。因此,调度 AGU uops 需要 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
完全匹配。
也可以对案例 3 进行类似于案例 2 的详细分析。在情况 3 中,STA 的执行与第二个加载的延迟重叠。两个负载的延迟也大多重叠。
我不知道为什么不同情况下的处罚不同。我们需要知道 L1D 缓存是如何精确设计的。无论如何,我有足够的信心,对加载延迟(和存储延迟)有"几个空闲周期"的惩罚来发布这个答案。
脚注
(1)另外80%的时间花在荷载矩阵上。手册中几乎没有提到这种结构。它用于指定 uops 和加载 uops 之间的依赖关系。据估计,SnB和IvB上有32个条目。没有记录在案的性能事件可以专门计算 LM 上的摊位。所有记录的资源停止事件均为零。在情况 3 中,每次迭代有 3 个 5 个 uops 取决于之前的负载,因此很可能在任何其他结构之前填充 LM。在IvB和SnB上,RS条目的"有效"数量估计分别约为51和48。
(2)我可能在这里做了一个无害的简化。请参阅RESOURCE_STALLS是否有可能。即使 RS 未完全满,也会发生 RS 事件。
(3) 创建通过管道的 uop 流的可视化可能会有所帮助,以了解它们如何组合在一起。您可以使用简单的负载链作为参考。这对于案例 1 很容易,但对于案例 2 来说,由于重播,这很困难。
分析方法2:使用负载延迟性能监视工具
我想出了另一种方法来分析代码。这种方法要容易得多,但不太准确。然而,它确实基本上使我们得出了同样的结论。
替代方法基于MEM_TRANS_RETIRED.LOAD_LATENCY_*
性能事件。这些事件是特殊的,因为它们只能在precise 级别进行计数(请参阅: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
了解这些数字表示所有载荷中随机选择的样本的载荷数至关重要。例如,所有负载的样本总大小为 1000 万,其中只有 100 万个延迟大于指定阈值,则测量值为 100 万。但是,执行的负载总数可能是 10 亿。因此,绝对值本身不是很有意义。真正重要的是跨不同阈值的模式。
在情况 1 中,延迟大于特定阈值的负载数有三次显著下降。我们可以推断出延迟等于或小于 6 个周期的负载是最常见的,延迟等于或小于 7 个周期但大于 6 个周期的负载是第二常见的负载,大多数其他负载的延迟在 8-11 个周期之间。
我们已经知道最小延迟是 4 个周期。给定这些数字,可以合理地估计平均负载延迟在 4 到 6 个周期之间,但更接近 6 而不是 4。我们从方法 1 中知道,平均负载延迟实际上是 5.4c。因此,我们可以使用这些数字做出相当好的估计。
在情况 2 中,我们可以推断大多数负载的延迟小于或等于 11 个周期。考虑到在广泛的延迟阈值范围内测量的负载数量的一致性,平均负载延迟也可能远大于 4。所以它在 4 到 11 之间,但比 4 更接近 11。我们从方法 1 中知道,平均负载延迟实际上是 8.8c,这接近基于这些数字的任何合理估计。
案例3 与案例 1 类似,实际上,使用方法 1 确定的实际平均负载延迟对于这两种情况几乎相同。
使用MEM_TRANS_RETIRED.LOAD_LATENCY_*
进行测量很容易,这种分析可以由对微架构知之甚少的人完成。