您没有标记您的操作系统,但我们假设您使用的是 Linux。这些东西在另一个操作系统上会有所不同(甚至可能在同一操作系统的各种变体中)。
在对未映射页面进行读取访问时,内核页面错误处理程序会映射到系统范围的共享零页面,并具有只读权限。
This explains columns LoadInit-1U|K
: even though your init load is striding over a virtual area of 64 MB performing loads, only a single physical 4K page filled with zeros is mapped, so you get approximately zero cache misses after the first 4KB, which rounds to zero after your normalization.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
events 查看缺失的事件是否出现在那里。
变化
不需要在所有系统上都这样。当然,其他操作系统可能有不同的策略,但即使是 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 执行时)但在其退出之前,为 (a) 存储发送 RFO 请求,或者 (b)对于存储缓冲区中接近但尚未到达存储缓冲区头部的存储。
显然,当存储到达缓冲区的头部时,就需要发送 RFO,并且您不会将其称为预取 - 但为什么不也发送一些对从头部开始的第二个存储的请求,等等(情况b)?或者为什么不知道存储地址后立即检查 L1D(就像加载一样),然后在未命中时发出推测性 RFO 预取?这些可能称为 RFO 预取,但它们与普通预取不同,因为核心knows所请求的地址:这不是猜测。
There is如果另一个核心在该核心有机会从中写入之前发送该行的 RFO,则获取除当前头之外的其他行可能会浪费工作:在这种情况下,该请求是无用的,只会增加一致性流量。因此,如果存储缓冲区预取过于频繁地失败,则有一些预测器可能会减少该预取。还可能存在这样的猜测:存储缓冲区预取可能会向尚未退休的初级存储发送请求,如果存储最终位于错误路径上,则会产生无用的请求。我实际上不确定当前的实现是否可以做到这一点。
1 This behavior actually depends on the details of the L1 cache: current Intel VIPT implementations allow multiple virutal aliases of the same single line to all live happily in L1. Current AMD Zen implementations use a different implementation (micro-tags) which don't allow the L1 to logically contain multiple virtual aliases, so I would expect Zen to miss to L2 in this case.