为什么只有在存储初始化循环时才计算用户模式L1存储未命中事件?

问题描述 投票:4回答:1

Summary

考虑以下循环:

loop:
movl   $0x1,(%rax)
add    $0x40,%rax
cmp    %rdx,%rax
jne    loop

其中rax初始化为大于L3缓存大小的缓冲区的地址。每次迭代都会对下一个缓存行执行存储操作。我期望从L1D发送到L2的RFO请求的数量或多或少等于所访问的高速缓存行的数量。问题是,当我计算内核模式事件时,即使程序在用户模式下运行,这似乎也是如此,除了在下面讨论的一种情况。缓冲区的分配方式似乎并不重要(.bss,.data或堆中)。

Details

我的实验结果如下表所示。所有实验均在禁用超线程且启用所有硬件预取程序的处理器上执行。

我测试了以下三种情况:

  • 没有初始化循环。也就是说,在上面显示的“主”循环之前不访问缓冲区。我将这个案例称为NoInit。在这种情况下只有一个循环。
  • 首先使用每个高速缓存行的一个加载指令来访问缓冲区。触摸完所有线后,执行主循环。我将这个案例称为LoadInit。在这种情况下有两个循环。
  • 首先使用每个高速缓存行的一个存储指令来访问缓冲区。触摸完所有线后,执行主循环。我将这个案例称为StoreInit。在这种情况下有两个循环。

下表显示了英特尔CFL处理器的结果。这些实验已在Linux内核版本4.4.0上执行。

enter image description here

下表显示了英特尔HSW处理器的结果。请注意,HSW没有记录事件L2_RQSTS.PF_HITL2_RQSTS.PF_MISSOFFCORE_REQUESTS.ALL_REQUESTS。这些实验已在Linux内核版本4.15上执行。

enter image description here

每个表的第一列包含性能监视事件的名称,其计数显示在其他列中。在列标签中,字母UK分别表示用户模式和内核模式事件。对于具有两个循环的情况,数字1和2分别用于指代初始化循环和主循环。例如,LoadInit-1K表示LoadInit情况的初始化循环的内核模式计数。

表中显示的值由高速缓存行的数量标准化。它们的颜色编码如下。绿色越深,该值相对于同一表中的所有其他单元格越大。但是,CFL表的最后三行和HSW表的最后两行没有颜色编码,因为这些行中的某些值太大。这些行涂成深灰色,表示它们没有像其他行那样进行颜色编码。

我期望用户模式L2_RQSTS.ALL_RFO事件的数量等于所访问的高速缓存行的数量(即,标准化值为1)。该事件在手册中描述如下:

计算到L2缓存的RFO(读取所有权)请求的总数。 L2 RFO请求包括L1D需求RFO未命中以及L1D RFO预取。

它说L2_RQSTS.ALL_RFO不仅可以计算来自L1D的RFO请求,还可以计算L1D RFO预取。但是,我观察到事件计数不受两个处理器上是否启用L1D预取程序的影响。但即使L1D预取程序可能生成RFO预取,事件计数也应该至少与访问的高速缓存行数一样大。从两个表中可以看出,这只是StoreInit-2U的情况。同样的观察结果适用于表格中显示的所有事件。

但是,事件的内核模式计数大约等于用户模式计数的预期值。这与例如MEM_INST_RETIRED.ALL_STORES(或HSW上的MEM_UOPS_RETIRED.ALL_STORES)形成对比,后者按预期工作。

由于PMU计数器寄存器数量有限,我不得不将所有实验分成四部分。特别是,内核模式计数是从与用户模式计数不同的运行产生的。在同一时间计算什么并不重要。我认为告诉你这一点很重要,因为这解释了为什么一些用户模式计数比同一事件的内核模式计数略大。

以深灰色显示的事件似乎有些过分。第四代和第八代英特尔处理器规范手册确实提到(问题分别为HSD61和111)OFFCORE_REQUESTS_OUTSTANDING.DEMAND_RFO可能超额计算。但是这些结果表明它可能被多次覆盖,而不仅仅是几个事件。

还有其他有趣的观察结果,但它们与问题无关,即:为什么RFO计数不如预期?

x86 intel performancecounter cpu-cache intel-pmu
1个回答
4
投票

您没有标记您的操作系统,但我们假设您使用的是Linux。这个东西在另一个操作系统上会有所不同(甚至可能在同一个操作系统的各种变体中)。

在对未映射页面的读访问时,内核页面错误处理程序映射到系统范围的共享零页面,具有只读权限。

这解释了LoadInit-1U|K列:即使您的初始化负载跨越64 MB的虚拟区域执行加载,也只映射了填充零的单个物理4K页面,因此在第一个4KB之后得到大约零缓存未命中,其舍入为零正常化后

在对未映射页面或只读共享零页面的写访问权限中,内核将代表进程映射新的唯一页面。这个新页面保证归零,所以除非内核有一些已知的零页面,这涉及在映射之前将页面归零(实际上是memset(new_page, 0, 4096))。

这在很大程度上解释了除StoreInit-2U|K之外的剩余列。在这些情况下,即使看起来用户程序正在执行所有存储,内核最终会完成所有的艰苦工作(每页一个存储除外),因为每个页面中的用户进程出错,内核写入零它具有将所有页面带入L1缓存的副作用。当故障处理程序返回时,该页面的触发存储和所有后续存储将在L1缓存中命中。

它仍然没有完全解释StoreInit-2。正如评论中所阐明的那样,K列实际上包括用户计数,这解释了该列(减去用户计数使得每个事件大致为零,如预期的那样)。剩下的困惑是为什么L2_RQSTS.ALL_RFO不是1,而是一些较小的值,如0.53或0.68。也许事件是计数不足,或者我们缺少一些微架构效应,比如一种防止RFO的预取(例如,如果在商店之前通过某种类型的加载操作将行加载到L1中,RFO不会发生)。您可以尝试包含其他L2_RQSTS事件,以查看丢失的事件是否显示在那里。

Variations

它不需要像所有系统那样。当然,其他操作系统可能有不同的策略,但即使是x86上的Linux也可能因各种因素而表现不同。

例如,您可能会获得2 MiB huge zero page而不是4K零页面。这将改变基准,因为2 MiB不适合L1,因此LoadInit测试可能会在第一个和第二个循环中显示用户空间中的未命中。

更一般地说,如果您使用大页面,页面错误粒度将从4 KiB更改为2 MiB,这意味着只有一小部分归零页面将保留在L1和L2中,因此您将获得L1和L2未命中,正如你所料。如果您的内核ever implements fault-around用于匿名映射(或您正在使用的任何映射),它可能会产生类似的效果。

另一种可能性是内核可能在后台零页面,因此准备好零页面。这将从测试中删除K计数,因为在页面错误期间不会发生归零,并且可能会将预期的未命中添加到用户计数中。我不确定Linux内核是否曾经这样做或者可以选择这样做,但是有patches floating around。像BSD这样的其他操作系统已经做到了。

RFO Prefetchers

关于“RFO预取器” - RFO预取器在通常意义上并不是真正的预取器,它们与L1D预取器无关,可以关闭。据我所知,来自L1D的“RFO预取”仅指在存储缓冲区中发送存储器的RFO请求,这些存储缓冲区正到达存储缓冲区的头部。很显然,当一个商店到达缓冲区的头部时,是时候发送一个RFO了,你不会把它称为预取 - 但为什么不发送一些请求到第二个来自头部的商店,依此类推?这些是RFO预取,但它们与正常的预取不同之处在于核心知道已经请求的地址:这不是猜测。

在某种意义上的猜测是,如果另一个核心在核心有机会写入之前发送该行的RFO,那么获得除当前头之外的额外行可能会浪费工作:在这种情况下请求没用,只是增加了一致性交通。因此,如果经常出现故障,预测器可能会减少此存储缓冲区预取。在某种意义上,也可能存在推测,即如果商店最终处于不良路径上,则存储缓冲器预取可以发送对尚未退休的初级商店的请求,代价是无用请求。我不确定当前的实现是否会这样做。


1这种行为实际上取决于L1缓存的细节:当前的英特尔VIPT实现允许相同单行的多个虚拟别名在L1中幸福地生活。当前的AMD Zen实现使用不同的实现(微标签),这些实现不允许L1在逻辑上包含多个虚拟别名,所以我希望Zen在这种情况下会错过L2。

© www.soinside.com 2019 - 2024. All rights reserved.