use*_*622 7 x86 assembly micro-optimization microbenchmark micro-architecture
首先,我在 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
Run Code Online (Sandbox Code Playgroud)
我插入到有效载荷位置:
mov qword [rax+8], 8
mov rax, [rax]
Run Code Online (Sandbox Code Playgroud)
perf显示循环为 5.4c/iter。有点理解,因为L1d延迟是4个周期。
我颠倒了这两条指令的顺序:
mov rax, [rax]
mov qword [rax+8], 8
Run Code Online (Sandbox Code Playgroud)
结果突然变成9c/iter。我不明白为什么。因为下一次迭代的第一条指令不依赖于当前迭代的第二条指令,所以这个设置应该和 case 1 没有区别。
我也用IACA工具对这两种情况进行静态分析,但是该工具不可靠,因为两种情况预测的结果都是5.71c/iter,与实验相矛盾。
然后我插入一条与mov案例 2无关的指令:
mov rax, [rax]
mov qword [rax+8], 8
mov rbx, [rax+16]
Run Code Online (Sandbox Code Playgroud)
现在结果变成了 6.8c/iter。但是一个无关的movinsert怎么把速度从9c/iter提升到6.8c/iter呢?
IACA 工具预测的结果与前一种情况相同,显示为 5.24c/iter。
我现在完全糊涂了,如何理解上述结果?
在情况 1 和 2 中,有一个地址rax+8。如果rax+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
Run Code Online (Sandbox Code Playgroud)
案例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%)
Run Code Online (Sandbox Code Playgroud)
案例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%)
Run Code Online (Sandbox Code Playgroud)
案例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%)
Run Code Online (Sandbox Code Playgroud)
案例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%)
Run Code Online (Sandbox Code Playgroud)
T1;DR:对于这三种情况,同时执行加载和存储时会产生几个周期的损失。在这三种情况下,加载延迟都位于关键路径上,但不同情况下的惩罚是不同的。由于额外的负载,情况 3 比情况 1 大约高一个周期。
分析方法一:利用失速性能事件
我能够在 IvB 和 SnB 的所有三个案例中重现您的结果。我得到的数字与你的数字相差不到2%。执行情况 1、2 和 4 的单次迭代所需的周期数分别为 5.4、8.9 和 6.6。
让我们从前端开始。和性能事件表明基本上所有的uop都是由LSD发出的LSD.CYCLES_4_UOPS。LSD.CYCLES_3_UOPS此外,这些活动还与LSD.CYCLES_ACTIVE表明,在 LSD 未停止的每个周期中,在情况 1 和 2 中发出 3 个微指令,在情况 3 中发出 4 个微指令。换句话说,正如预期的那样,每次迭代的微指令在同一组中在一个周期内一起发布。
以下所有关系中,“=~”符号表示差异在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 造成的停顿约占所有资源停顿的 20% 1。
现在让我们关注情况 1。总共有 4 个未融合域 uop:1 个加载 uop、1 个 STA、1 个 STD 和 1 个 dec/jne。负载和 STA uop 取决于之前的负载 uop。每当LSD发出一组uop时,STD和jump uop都可以在下一个周期调度,因此下一个周期不会导致执行停顿事件。然而,可以调度负载和STA微指令的最早点是在负载结果被写回的同一周期中。CYCLES_NO_EXECUTE和之间的相关性STALLS_LDM_PENDING表明,没有准备好执行的微指令的原因是因为 RS 中的所有微指令都在等待 L1 来服务挂起的加载请求。具体来说,RS中的uop一半是加载uop,另一半是STA,它们都在等待各自先前迭代的加载完成。LSD.CYCLES_3_UOPS表明LSD等待RS中至少有4个空闲条目,然后才发出一组构成完整迭代的uop。在下一个周期中,将调度其中两个微指令,从而释放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?
调度程序将预测微指令所依赖的负载将在某个恒定的延迟内完成,因此它将相应地调度它们。如果由于任何原因(例如 L1 未命中)加载花费的时间超过了加载时间,则会调度微指令,但加载结果尚未到达。在这种情况下,uop将被重放,并且发送的uop数量将大于发送的uop总数。
负载和 STA 微指令只能调度到端口 2 或 3。事件UOPS_EXECUTED_PORT.PORT_2和UOPS_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微指令总数正好等于退役的AGU微指令数量;没有重播。所以调度程序永远不会错误预测。在情况 2 中,每个 AGU uop 平均有 2 次重播,这意味着调度程序平均每个 AGU uop 错误预测两次。为什么情况 2 会出现错误预测,而情况 1 却不会?
由于以下任一原因,调度程序将根据负载重放 uops:
前5个原因可以通过相应的性能事件明确排除。Patrick Fay(英特尔)说道:
最后,是的,在加载和存储之间切换时存在“一些”空闲周期。我被告知不要比“一些”更具体。
...
SNB 可以在同一周期读取和写入不同的存储体。
我发现这些陈述可能是故意的,有点含糊不清。第一个陈述表明 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 的周期)。我们需要添加 2 个周期,CYCLE_ACTIVITY.STALLS_LDM_PENDING因为这些周期中没有执行停顿,但它们只占总加载延迟的一小部分。这等于 6.8 + 2 = 8.8 个周期 =~ cycles。
在执行前十几次(左右)迭代期间,每个周期都会在 RS 中分配一个跳转和 STD uop。这些将始终在发布周期之后的周期中调度执行。在某个时刻,RS 将变满,所有尚未分派的条目都将成为 STA 并加载微指令,等待相应先前迭代的加载微指令完成(写回其结果)。因此分配器将停止,直到有足够的空闲 RS 条目来发出整个迭代。假设最旧的加载 uop 已在周期写回其结果T+ 0 处写回其结果。我将该加载 uop 所属的迭代称为当前迭代。将发生以下事件序列:
循环时T + 0时:调度当前迭代的STA uop和下一次迭代的load uop。由于没有足够的 RS 条目,因此此周期中没有分配。该周期被计为分配停顿周期,但不计为执行停顿周期。
循环时T+ 1 处:STA uop 完成执行并且存储退出。分配下一次要分配的迭代的微指令。该周期被计为执行停顿周期,但不计为分配停顿周期。
循环时T+ 2 处:刚刚分配的跳转和 STD uop 被调度。该周期被计为分配停顿周期,但不计为执行停顿周期。
在周期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个融合微指令。uop 在进入 RS 时未熔断。1 个 STD 需要 1 个周期才能调度。跳转也需要1个周期。有 3 个 AGU uop,但只有 2 个 AGU 端口。因此,需要 2 个周期(与情况 1 和 2 中的 1 个周期相比)来调度 AGU uop。调度的 AGU uop 组将是以下之一:
由于需要多一个周期才能释放足够的 RS 条目来容纳整个问题组,因此UOPS_ISSUED.STALL_CYCLES+ 1 - 1 = UOPS_ISSUED.STALL_CYCLES=~ 平均加载延迟 =~ 5.6c,这与情况 1 非常接近。惩罚约为 1.6c 。这解释了为什么与情况 1 和 2 相比,情况 3 中每个 AGU uop 平均调度 1.4 次。
同样,由于需要更多的周期来释放足够的 RS 条目来容纳整个问题组:
cycles=~ 平均负载延迟 + 1 = 6.6c/iter,这实际上cycles与我的系统上测量的完全匹配。
对案例3也可以进行与案例2类似的详细分析。在情况 3 中,STA 的执行与第二次加载的延迟重叠。两个负载的延迟也大部分重叠。
我不知道为什么不同情况下处罚不同。我们需要知道 L1D 缓存是如何设计的。不管怎样,我有足够的信心,在发布这个答案时,加载延迟(和存储延迟)会受到“一些空闲周期”的惩罚。
脚注
(1) 另外 80% 的时间花在负载矩阵上。该结构在手册中几乎没有提及。它用于指定微指令和加载微指令之间的依赖关系。SnB 和 IvB 估计有 32 个条目。没有记录的性能事件可以专门计算 LM 上的失速。所有记录的资源停顿事件均为零。在情况 3 中,每次迭代有 5 个微指令中的 3 个取决于先前的负载,因此很可能 LM 将在任何其他结构之前被填充。IvB 和 SnB 上的 RS 条目的“有效”数量估计分别约为 51 和 48。
(2) 我可能在这里做了无害的简化。请参阅即使 RS 未完全满,是否也可能发生 RESOURCE_STALLS.RS 事件?。
(3) 创建通过管道的 uop 流的可视化可能会有所帮助,以了解这一切如何组合在一起。您可以使用简单的负载链作为参考。这对于情况 1 来说很容易,但对于情况 2 来说由于重播而很困难。
分析方法 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
Run Code Online (Sandbox Code Playgroud)
理解这些数字代表所有负载中随机选择的样本的负载数量至关重要。例如,所有负载的样本总大小为 1000 万,其中只有 100 万的延迟大于指定阈值,则测量值为 100 万。然而,执行的负载总数可能是 10 亿。因此,绝对值本身并没有多大意义。真正重要的是跨越不同阈值的模式。
在情况 1 中,延迟大于特定阈值的负载数量出现了 3 次显着下降。我们可以推断,延迟等于或小于 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_*很容易,并且这种分析可以由对微体系结构知之甚少的人完成。
| 归档时间: |
|
| 查看次数: |
393 次 |
| 最近记录: |