Had*_*ais 6 x86 intel performancecounter cpu-cache intel-pmu
考虑以下循环:
loop:
movl $0x1,(%rax)
add $0x40,%rax
cmp %rdx,%rax
jne loop
Run Code Online (Sandbox Code Playgroud)
whererax被初始化为大于 L3 缓存大小的缓冲区的地址。每次迭代都会对下一个缓存行执行存储操作。我希望从 L1D 发送到 L2 的 RFO 请求数量或多或少等于访问的缓存线数量。问题是,即使程序在用户模式下运行,这似乎也只是当我计算内核模式事件时的情况,除非我在下面讨论的一种情况。缓冲区的分配方式似乎无关紧要(.bss、.data 或来自堆)。
我的实验结果如下表所示。所有实验都是在禁用超线程和启用所有硬件预取器的处理器上进行的。
我测试了以下三种情况:
NoInit. 在这种情况下只有一个循环。LoadInit. 在这种情况下有两个循环。StoreInit. 在这种情况下有两个循环。下表显示了英特尔 CFL 处理器上的结果。这些实验是在 Linux 内核版本 4.4.0 上进行的。
下表显示了英特尔 HSW 处理器上的结果。请注意,HSW 未记录事件L2_RQSTS.PF_HIT、L2_RQSTS.PF_MISS和OFFCORE_REQUESTS.ALL_REQUESTS。这些实验是在 Linux 内核版本 4.15 上进行的。
每个表的第一列包含性能监控事件的名称,其计数显示在其他列中。在列标签中,字母U和 分别K代表用户模式和内核模式事件。对于有两个循环的情况,数字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 代英特尔处理器规格手册确实提到(分别是 HSD61 和 111 问题),这OFFCORE_REQUESTS_OUTSTANDING.DEMAND_RFO可能会高估。但这些结果表明,它可能被多次高估,而不仅仅是几个事件。
还有其他有趣的观察结果,但它们与问题无关,即:为什么 RFO 计数不符合预期?
您没有标记您的操作系统,但让我们假设您使用的是 Linux。这些东西在另一个操作系统上会有所不同(甚至可能在同一操作系统的各种变体中)。
在对未映射页面进行读访问时,内核页面错误处理程序映射到系统范围的共享零页面中,具有只读权限。
这解释了列LoadInit-1U|K:即使您的 init 加载跨越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 预取器并不是通常意义上的真正预取器,它们与 L1D 预取器无关,可以关闭。据我所知,来自 L1D 的“RFO 预取”只是指在计算地址时(即,当存储数据 uop 执行时)为(a)存储发送 RFO 请求,但在它退休之前或(b)用于存储缓冲区中接近但尚未到达存储缓冲区头部的存储。
显然,当一个存储到达缓冲区的头部时,是时候发送一个 RFO,你不会把它称为预取——但为什么不也发送一些对第二个从头开始的存储的请求,依此类推(情况b)?或者为什么不在知道存储地址后立即检查 L1D(就像加载一样),然后在未命中时发出推测性 RFO 预取?这些可能被称为 RFO 预取,但它们与普通预取的不同之处在于内核知道已请求的地址:这不是猜测。
还有就是炒作的意义上,获得比目前的头部以外的其他线路可能被浪费的工作,如果另一处理器发送该行的RFO该核心拥有前有机会写下来:请求是在这种情况下没用,只是增加了一致性交通。因此,如果此存储缓冲区预取过于频繁失败,则有一些预测器可能会减少此存储缓冲区预取。在某种意义上也可能存在猜测,即存储缓冲区预取可能会向尚未退休的初级存储发送请求,代价是如果存储最终走在一条错误的路径上,则会产生无用的请求。我实际上不确定当前的实现是否这样做。
1这种行为实际上取决于 L1 缓存的细节:当前的英特尔 VIPT 实现允许同一单行的多个虚拟别名都愉快地存在于 L1 中。当前的 AMD Zen 实现使用不同的实现(微标签),它不允许 L1 在逻辑上包含多个虚拟别名,所以我希望 Zen 在这种情况下会错过 L2。