6

概括

考虑以下循环:

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

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

细节

我的实验结果如下表所示。所有实验都是在禁用超线程并启用所有硬件预取器的处理器上进行的。

我测试了以下三种情况:

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

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

在此处输入图像描述

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

在此处输入图像描述

每个表的第一列包含性能监控事件的名称,其计数显示在其他列中。在列标签中,字母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(或MEM_UOPS_RETIRED.ALL_STORES在 HSW 上)按预期工作相反。

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

以深灰色显示的事件似乎多虑了。第 4 代和第 8 代 Intel 处理器规格手册确实提到了(分别是问题 HSD61 和 111)OFFCORE_REQUESTS_OUTSTANDING.DEMAND_RFO可能会高估。但这些结果表明,它可能被高估了很多倍,而不仅仅是几个事件。

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

4

1 回答 1

6

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

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

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

在对未映射页面或只读共享零页面进行写访问时,内核将代表进程映射一个新的唯一页面。这个新页面保证会被清零,因此除非内核有一些已知为零的页面,否则这涉及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事件以查看丢失的事件是否显示在那里。

变化

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

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

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

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

RFO 预取器

关于“RFO 预取器”——RFO 预取器并不是通常意义上的真正预取器,它们与 L1D 预取器无关,可以关闭。据我所知,来自 L1D 的“RFO 预取”只是指在计算其地址时(即,当存储数据 uop 执行时),但在它退休或(b)之前发送一个 RFO 请求,用于(a)存储用于存储缓冲区中接近但尚未到达存储缓冲区头部的存储。

显然,当一个存储到达缓冲区的头部时,是时候发送一个 RFO,您不会将其称为预取 - 但为什么不发送一些对第二个头部存储的请求,等等(案例b)?或者为什么不在存储地址已知后立即检查 L1D(如加载那样),然后在未命中时发出推测性 RFO 预取?这些可能被称为 RFO 预取,但它们与普通预取的不同之处在于内核知道已请求的地址:这不是猜测。

某种意义上说,如果另一个核心在核心有机会写入该行之前为该行发送 RFO,则获取除当前头部之外的其他行可能会浪费工作:在这种情况下,请求是无用的,只是增加了一致性交通。因此,如果它经常失败,有一些预测器可能会减少此存储缓冲区预取。在某种意义上也可能存在推测,即存储缓冲区预取可能会向尚未退役的初级存储发送请求,如果存储最终处于错误的路径上,则会以无用的请求为代价。我实际上不确定当前的实现是否这样做。


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

于 2019-03-05T05:39:30.590 回答