为什么每次迭代的uops数量会随着流量负载的增加而增加?

Had*_*ais 9 x86 assembly cpu-architecture intel-pmu

考虑以下循环:

.loop:
    add     rsi, OFFSET    
    mov     eax, dword [rsi]
    dec     ebp
    jg .loop
Run Code Online (Sandbox Code Playgroud)

where OFFSET是一些非负整数,并rsi包含指向该bss部分中定义的缓冲区的指针.此循环是代码中唯一的循环.也就是说,它在循环之前没有被初始化或触摸.据推测,在Linux上,缓冲区的所有4K虚拟页面都将按需映射到同一物理页面.因此,缓冲区大小的唯一限制是虚拟页面的数量.因此,我们可以轻松地尝试非常大的缓冲区.

该循环由4条指令组成.每个指令在Haswell的融合和未融合域中被解码为单个uop.连续的实例之间也存在循环携带的依赖关系add rsi, OFFSET.因此,在负载总是在L1D中命中的空闲条件下,循环应该每次迭代执行大约1个循环.对于小偏移(步幅),这要归功于基于IP的L1流预取器和L2流预取器.但是,两个预取程序只能在4K页面内预取,并且L1预取程序支持的最大步幅为2K.因此,对于小步幅,每4K页面应该有大约1 L1未命中.随着步幅的增加,L1未命中和TLB未命中的总数将增加,并且性能将相应地恶化.

下图显示了0到128之间步幅的各种有趣的性能计数器(每次迭代).请注意,所有实验的迭代次数都是常量.仅缓冲区大小更改以适应指定的步幅.此外,仅计算用户模式性能事件.

在此输入图像描述

这里唯一奇怪的事情是退役的uops数量随着步伐的增加而增加.它从每次迭代3次(如预期)到步幅128的11次.为什么?

如下图所示,事情只会越来越大.在此图中,步幅范围为32到8192,增量为32字节.首先,退出指令的数量在步长4096字节处从4线性增加到5,之后它保持不变.负载微量的数量从1增加到3,并且每次迭代L1D负载命中的数量保持为1.对于所有步幅,只有L1D负载未命中数才对我有意义.

在此输入图像描述

较大步幅的两个明显效果是:

  • 执行时间增加,因此会发生更多的硬件中断.但是,我正在计算用户模式事件,因此中断不应该干扰我的测量.我也用taskset或重复了所有实验,nice并得到了相同的结果.
  • 页面遍历和页面错误的数量增加.(我已经对此进行了验证,但为了简洁,我将省略这些图.)页面错误由内核模式下的内核处理.根据这个答案,使用专用硬件(Haswell?)实现页面遍历.虽然答案基于的链接已经死亡.

为了进一步调查,下图显示了微代码辅助的uop数.每次迭代的微代码辅助微动的数量增加,直到它达到步幅4096的最大值,就像其他性能事件一样.对于所有步幅,每4K虚拟页面的微代码辅助微操作数为506."Extra UOPS"行显示退役的uop数减去3(每次迭代的预期uop数).

在此输入图像描述

该图表示额外微量的数量略大于所有步幅的微码辅助微量的一半.我不知道这意味着什么,但它可能与页面走路有关,可能是观察到扰动的原因.

为什么即使每次迭代的静态指令数相同,每次迭代的退出指令和uop的数量也会增加?来自哪里的干扰?


下图显示了每次迭代的周期数与不同步幅的每次迭代的退役uop数.循环次数的增加比退役的次数增加得快得多.通过使用线性回归,我发现:

cycles = 0.1773 * stride + 0.8521
uops = 0.0672 * stride + 2.9277
Run Code Online (Sandbox Code Playgroud)

采用两种功能的衍生物:

d(cycles)/d(stride) = 0.1773
d(uops)/d(stride) = 0.0672
Run Code Online (Sandbox Code Playgroud)

这意味着循环次数增加0.1773,退役微量数量增加0.0672,步幅每增加1个字节.如果中断和页面错误确实是(唯一)扰动的原因,那么两个速率是否应该非常接近?

在此输入图像描述

在此输入图像描述

Bee*_*ope 7

你在许多性能计数器中反复看到的效果,其中值线性增加直到它保持不变的步幅4096,如果你认为效果完全是由于增加的步幅增加页面错误,那么这是完全有意义的.页面错误会影响观察到的值,因为许多计数器在存在中断,页面错误等情况下并不精确.

例如,instructions当你从步幅0进展到4096时,从4到5 的计数器上升.我们从其他来源得知,Haswell上的每个页面错误都会在用户模式下计算一个额外的指令(在内核模式下也会有一个额外的指令) .

因此,我们期望的指令数量是循环中4个指令的基础,加上一些指令的一部分,基于每个循环我们采用的页面错误数量.如果我们假设每个新的4 KiB页面导致页面错误,则每次迭代的页面错误数量为:

MIN(OFFSET / 4096, 1)
Run Code Online (Sandbox Code Playgroud)

由于每个页面错误都会计算额外的指令,因此我们可以获得预期的指令数:

4 + 1 * MIN(OFFSET / 4096, 1)
Run Code Online (Sandbox Code Playgroud)

这与您的图表完全一致.

因此,对于所有计数器,立即解释了倾斜图形的粗略形状:斜率仅取决于每页错误的计数量.然后唯一剩下的问题是为什么页面错误会以您确定的方式影响每个计数器.我们已经介绍instructions了,但让我们看看其他的:

MEM_LOAD_UOPS.L1_MISS

每页只有1个错过,因为只有接触下一页的负载错过了任何东西(它需要一个错误).我实际上并不认为L1预取器不会导致其他错过:我认为如果关闭预取器,你会得到相同的结果.我认为你没有得到更多L1未命中,因为相同的物理页面支持每个虚拟页面,一旦你添加了TLB条目,所有行都已经在L1中(第一次迭代将错过 - 但我想你正在进行多次迭代).

MEM_UOPS_RETIRED.ALL_LOADS

这表示每页错误3个uop(2个额外).

我不是100%确定这个事件在uop重播的情况下是如何工作的.它是否总是根据指令计算固定数量的uop,例如,你在Agner的指令中看到的数字 - > uop表?或者它是否计算代表指令发送的uops的实际数量?这通常是相同的,但是当它们错过各种缓存级别时,加载会重放它们的uops.

例如,我发现在Haswell和Skylake 2上,当L1中的负载未命中但是在L2中命中时,您会在负载端口(端口2和端口3)之间看到总共2个微指令.据推测,发生的事情是uop被假设它将在L1中命中,并且当没有发生时(结果在调度程序预期时没有准备好),它会被预期L2命中的新时间重放.这是"轻量级的",因为它不需要任何类型的管道清除,因为没有执行错误路径指令.

类似地,对于L3未命中,我已经观察到每个负载3个uop.

鉴于此,似乎有理由假设新页面上的未命中导致加载uop被重放两次(正如我所观察到的),并且那些uops出现在MEM_UOPS_RETIRED计数器中.有人可能会合理地争辩说,重播的微博并没有退休,但从某种意义上说,退休与指令的关系比uops更多.也许这个计数器可以更好地描述为"与退役加载指令相关联的调度uop".

UOPS_RETIRED.ALLIDQ.MS_UOPS

剩下的奇怪之处在于与每个页面相关的大量uops.似乎完全有可能这与页面错误机制有关.您可以尝试在TLB中错过的类似测试,但不会出现页面错误(确保页面已经填充,例如,使用mmapwith MAP_POPULATE).

之间的差异MS_UOPSUOPS_RETIRED似乎并不奇怪,因为一些微指令可能不退休.也许他们也算在不同的领域(我忘了如果UOPS_RETIRED是融合或未融合的域).

在这种情况下,可能在用户和内核模式计数之间也存在泄漏.

周期与uop衍生物

在你的问题的最后部分,你表明周期与偏移的"斜率"比退役的uops与偏移的斜率大约2.6倍.

如上所述,此处的效果在4096处停止,我们再次预期此效果完全是由页面错误引起的.因此,斜率的差异只意味着页面错误的成本比uops多2.6倍.

你说:

如果中断和页面错误确实是(唯一)扰动的原因,那么两个速率是否应该非常接近?

我不明白为什么.uops和周期之间的关系可能差别很大,可能有三个数量级:CPU可能每个周期执行四次uop,或者执行单个uop可能需要100个周期(例如缓存缺失负载).

每个uop 2.6个循环的值正好在这个大范围的中间,并没有让我感到奇怪:它有点高(如果你谈论优化的应用程序代码,"效率低下")但在这里我们谈论的是页面故障处理是一个完全不同的事情,所以我们期待长时间的延迟.

研究过度计数

任何对由于页面错误和其他事件导致的过度计数感兴趣的人可能会对这个github存储库感兴趣,该存储库对各种PMU事件的"确定性"进行了详尽的测试,并且已经注意到许多这种性质的结果,包括Haswell.然而它并没有涵盖哈迪在这里提到的所有计数器(否则我们已经有了答案).这是相关论文和一些易于使用的相关幻灯片 - 他们特别提到每页错误会产生一条额外的指令.

以下是英特尔的结果报价:

Conclusions on the event determinism:
1.  BR_INST_RETIRED.ALL (0x04C4)
a.  Near branch (no code segment change): Vince tested 
    BR_INST_RETIRED.CONDITIONAL and concluded it as deterministic. 
    We verified that this applies to the near branch event by using 
    BR_INST_RETIRED.ALL - BR_INST_RETIRED.FAR_BRANCHES.
b.  Far branch (with code segment change): BR_INST_RETIRED.FAR_BRANCHES 
    counts interrupts and page-faults. In particular, for all ring 
    (OS and user) levels the event counts 2 for each interrupt or 
    page-fault, which occurs on interrupt/fault entry and exit (IRET).
    For Ring 3 (user) level,  the counter counts 1 for the interrupt/fault
    exit. Subtracting the interrupts and faults (PerfMon event 0x01cb and
    Linux Perf event - faults), BR_INST_RETIRED.FAR_BRANCHES remains a 
    constant of 2 for all the 17 tests by Perf (the 2 count appears coming
    from the Linux Perf for counter enabling and disabling). 
Consequently, BR_INST_RETIRED.FAR_BRANCHES is deterministic. 
Run Code Online (Sandbox Code Playgroud)

因此,对于每页错误,您需要一条额外的指令(特别是分支指令).


1 在许多情况下,这种"不精确"仍然是确定性的 - 因为在外部事件存在的情况下,计数过多或计数不足总是以相同的方式运行,因此如果您还跟踪了多少,您也可以纠正它相关事件已经发生.

2我并不是要将它限制在这两个微架构中:它们恰好是我测试过的.

  • 最后,我在电子表格中发现了一个错误.我正在计算`(每页uops - (每页3*个指令))`而不是`(每页uops - (每页3次迭代))`.所有步幅的uop数均为274%.现在考虑`(每页指令 - (每页4次迭代))`.它在步幅512处迅速变得平坦.在步幅32处它是0.26然后它增加直到它在步幅512和之后达到1. (2认同)