gpt4 book ai didi

assembly - 在IvyBridge上的指针追逐循环中,附近的从属存储产生了奇怪的性能影响。添加额外的负载可以加快速度吗?

转载 作者:行者123 更新时间:2023-12-03 14:43:37 29 4
gpt4 key购买 nike

首先,我在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。如果将 rax+8更改为 rax+16rax+24,则情况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
[rax+8]的情况1:
 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%)
[rax+8]的情况3:
 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%)

最佳答案

T1; DR:对于这三种情况,在同时执行加载和存储时会产生几个周期的损失。在这三种情况下,负载等待时间都位于关键路径上,但是在不同情况下,代价是不同的。由于额外的负载,情况3比情况1高出一个周期。

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

对于IvB和SnB的所有三种情况,我都能够重现您的结果。我得到的数字在您数字的2%以内。执行情况1、2和4的单次迭代所需的周期数分别是5.4、8.9和6.6。

让我们从前端开始。 LSD.CYCLES_4_UOPSLSD.CYCLES_3_UOPS性能事件表明,基本上所有uops都是从LSD发出的。此外,这些事件与LSD.CYCLES_ACTIVE一起显示,在LSD没有停止的每个周期中,情况1和2发出3 uops,情况3发出4 uops。换句话说,每个迭代在单个周期中在同一组中一起发布。

在以下所有关系中,“=〜”符号表示差异在2%以内。我将从以下经验观察开始:
UOPS_ISSUED.STALL_CYCLES + LSD.CYCLES_ACTIVE =〜cycles
请注意,如here中所述,需要调整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个未融合的域uops:1个负载uop,1个STA,1个STD和1个dec/jne。负载和STA负载取决于先前的负载。每当LSD发出一组uops时,就可以在下一个周期中调度STD和跳转uops,因此下一个周期不会导致执行停止事件。但是,可以分派(dispatch)负载和STA uops的最早时间是在将负载结果回写的同一周期中。 CYCLES_NO_EXECUTESTALLS_LDM_PENDING之间的相关性表明,将没有准备就绪的uops的原因是因为RS中的所有uops都在等待L1为未决的负载请求提供服务。具体而言,RS中的微指令的一半是负载微指令,另一半是STA,它们都在等待相应的先前迭代的负载完成。 LSD.CYCLES_3_UOPS显示LSD等待直到RS中至少有4个空闲条目,然后才发出一组构成完整迭代的uops。在下一个周期中,将分派(dispatch)其中的两个微指令,从而释放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?

调度程序将预测uoop所依赖的负载将在某个恒定的延迟内完成,因此它将调度它们以进行相应的调度。如果由于某种原因(例如L1丢失)而使装载花费的时间超过了装载时间,则将派出uops,但装载结果尚未到达。在这种情况下,将重播这些微指令,并且已分配的微指令的数量将大于已发出的微指令的总数。

只能将load和STA uops调度到端口2或3。事件UOPS_EXECUTED_PORT.PORT_2UOPS_EXECUTED_PORT.PORT_3可以分别用来计算分发到端口2和3的uops的数量。

情况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中却没有?

出于以下任何原因,调度程序将根据负载重播uops:

  • L1高速缓存未命中。
  • 内存歧义错误预测。
  • 内存一致性冲突。
  • L1高速缓存命中,但是存在L1-L2流量。
  • 虚拟页号预测错误。
  • 其他一些原因(未记录)。

  • 可以使用相应的性能事件来明确排除前5个原因。 Patrick Fay(Intel) says以下内容:

    Lastly yes, there are 'a few' idle cycles when switching between a load and a store. I'm told not to be more specific than 'a few'.
    ...
    SNB can read and write different banks at the same cycle.



    我发现这些陈述,也许是故意的,有些含糊。第一条语句表明对L1的加载和存储永远不会完全重叠。第二个建议是,只有存在不同的存储库时,才可以在同一周期内执行加载和存储。虽然去不同的银行可能既不是必要条件也不是充分条件。但是可以肯定的是,如果同时存在加载和存储请求,则可以将加载(和存储)延迟一个或多个周期。这说明了情况1中平均1.4c的负载延迟损失。

    情况1和情况2之间存在差异。在情况1中,取决于同一负载uop的STA和负载uop在同一周期内一起发出。另一方面,在情况2中,依赖于相同负载uop的STA和负载uop属于两个不同的问题组。每次迭代的问题停顿时间基本上等于顺序执行一个加载并退出一个存储所花费的时间。可以使用 CYCLE_ACTIVITY.STALLS_LDM_PENDING估算每个操作的贡献。执行STA uop需要一个周期,因此商店可以在紧跟STA派发STA的周期之后退出。

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

    在执行前十二个(或大约)迭代期间,将在每个周期中在RS中分配一个跳转和STD指令。这些问题将始终在发布周期之后的周期中分派(dispatch)以便执行。在某个时候,RS将满载,所有尚未调度的条目将是STA和正在等待相应先前迭代的加载操作完成的加载加载(写回其结果)。因此,分配器将停顿,直到有足够的可用RS条目来发出整个迭代为止。假设最旧的加载uop已将其结果写回周期 T +0。我将把该加载uop所属的迭代称为当前迭代。将发生以下事件序列:

    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,所以一次迭代的uops都可以在一个周期内分配。微件在进入RS时不会融合。 1个STD需要调度1个周期。跳跃也需要1个周期。有3个AGU主机,但只有2个AGU端口。因此,需要2个周期(在情况1和2下为1个周期)来调度AGU uops。派发的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和load uops之间的依赖关系。 SnB和IvB上有32个条目是 estimated。没有记录在案的性能事件可以专门统计LM上的停顿。所有已记录的资源停顿事件均为零。在情况3中,每次迭代有3出5微妙,这取决于先前的负载,因此最有可能在所有其他结构之前填充LM。 RS条目的“有效”数量在IvB和SnB上分别约为51和48。

    (2)我可能在这里做了无害的简化。参见 Is it possible for the RESOURCE_STALLS.RS event to occur even when the RS is not completely full?

    (3)创建通过管道的uop流的可视化以查看所有这些如何融合可能会有所帮助。您可以使用简单的负载链作为引用。这对于情况1来说很容易,但是由于重放,对于情况2来说很难。

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

    我想出了另一种方法来分析代码。这种方法容易得多,但准确性较低。但是,它的确从本质上使我们得出相同的结论。

    替代方法基于 MEM_TRANS_RETIRED.LOAD_LATENCY_*性能事件。这些事件是特殊的,因为它们只能在 p Recise级别上计数(请参阅: PERF STAT does not count memory-loads but counts memory-stores)。

    例如, 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个周期之间,但比4接近6。我们从方法1中知道,平均负载延迟实际上是5.4c。因此,我们可以使用这些数字进行相当不错的估算。

    在情况2中,我们可以推断出大多数负载的等待时间都小于或等于11个周期。考虑到在广泛的延迟阈值范围内测得的负载数量的一致性,平均负载延迟可能也比4大得多。因此它在4到11之间,但比4更接近11。我们从方法1中知道,平均负载延迟实际上是8.8c,这接近于基于这些数字的任何合理估计。

    情况3与情况1类似,实际上,对于这两种情况,使用方法1确定的实际平均负载等待时间几乎相同。

    使用 MEM_TRANS_RETIRED.LOAD_LATENCY_*进行测量很容易,并且这种分析可以由对微体系结构了解很少的人来完成。

    关于assembly - 在IvyBridge上的指针追逐循环中,附近的从属存储产生了奇怪的性能影响。添加额外的负载可以加快速度吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54084992/

    29 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com