考虑以下循环:
loop:
movl $0x1,(%rax)
add $0x40,%rax
cmp %rdx,%rax
jne loop
其中rax
初始化为大于L3高速缓存大小的缓冲区的地址。每次迭代都会对下一个缓存行执行存储操作。我希望从L1D发送到L2的RFO请求的数量或多或少等于访问的缓存行的数量。问题是,即使程序在用户模式下运行,我也只在计数内核模式事件时才出现这种情况,下面将讨论一种情况。分配缓冲区的方式似乎无关紧要(.bss,.data或来自堆)。
我的实验结果显示在下表中。所有实验都是在禁用超线程并启用所有硬件预取器的处理器上进行的。
我已经测试了以下三种情况:
NoInit
。在这种情况下,只有一个循环。LoadInit
。在这种情况下有两个循环。StoreInit
。在这种情况下有两个循环。下表显示了在Intel CFL处理器上的结果。这些实验是在Linux内核4.4.0版上进行的。
下表显示了在Intel HSW处理器上的结果。请注意,事件L2_RQSTS.PF_HIT
,L2_RQSTS.PF_MISS
和OFFCORE_REQUESTS.ALL_REQUESTS
没有记录到HSW。这些实验是在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
(或HSW上的MEM_UOPS_RETIRED.ALL_STORES
)相反,后者按预期工作。
由于PMU计数器寄存器的数量有限,我不得不将所有实验分为四个部分。特别是,内核模式计数是从与用户模式计数不同的运行中产生的。相同的内容实际上并不重要。我认为告诉您这一点很重要,因为这解释了为什么某些用户模式计数比相同事件的内核模式计数大一些。
以深灰色显示的事件似乎过多。英特尔第四代和第八代处理器规范手册确实提到了OFFCORE_REQUESTS_OUTSTANDING.DEMAND_RFO
可能过高的问题(分别是问题HSD61和111)。但是这些结果表明,它可能被多次计数,而不仅仅是一些事件。
还有其他有趣的发现,但它们与问题无关,即:为什么RFO计数不如预期?
答案 0 :(得分:4)
您没有标记操作系统,但是假设您使用的是Linux。在其他操作系统上(甚至可能在同一操作系统的各种变体中),这些东西也会有所不同。
对未映射页面的读取访问时,内核页面故障处理程序将以只读权限映射到系统范围的共享零页面。
这说明了列LoadInit-1U|K
:即使您的初始负载跨越了执行负载的64 MB的虚拟区域,但仅填充了一个 physical 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 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预取器”-RFO预取器并不是通常意义上的预取器,它们与L1D预取器无关,可以关闭。据我所知,从L1D进行“ RFO预取”只是指向存储缓冲区中到达存储缓冲区头部的存储发送RFO请求。显然,当存储到达缓冲区的头部时,该发送RFO了,您不会将其称为预取-但是为什么不发送对第二个从头存储的请求,依此类推?这些是RFO预取,但是它们与常规预取的不同之处在于核心知道已请求的地址:这不是猜测。
有一种猜测是,如果另一个内核在有机会从该行写入之前发送另一行的RFO,则获取当前行头以外的其他行可能会被浪费。在这种情况下,请求是没有用的,只是增加了一致性流量。因此,如果预测失败的次数太多,可能会减少此存储缓冲区的预取。从某种意义上说,在存储缓冲区预取可能发送对尚未退休的初级存储的请求的意义上,也可能是猜测,如果存储最终处于错误的路径,则会以无用的请求为代价。我实际上不确定当前的实现是否可以做到这一点。
1 这种行为实际上取决于L1缓存的详细信息:当前的Intel VIPT实现允许同一行的多个虚拟别名有效地存在于L1中。当前的AMD Zen实施使用不同的实施(微标签),这种实施不允许L1在逻辑上包含多个虚拟别名,因此我希望Zen在这种情况下会错过L2。