为什么用户态 L1 store miss 事件只在有 store initialization loop 时才被统计?
Why are the user-mode L1 store miss events only counted when there is a store initialization loop?
总结
考虑以下循环:
loop:
movl [=10=]x1,(%rax)
add [=10=]x40,%rax
cmp %rdx,%rax
jne loop
其中 rax
被初始化为大于 L3 缓存大小的缓冲区地址。每次迭代都对下一个缓存行执行存储操作。我希望从 L1D 发送到 L2 的 RFO 请求数或多或少等于访问的缓存行数。问题是,这似乎只是当我计算内核模式事件时的情况,即使程序在用户模式下运行,除了我在下面讨论的一种情况。缓冲区的分配方式似乎并不重要(.bss、.data 或来自堆)。
详情
我的实验结果显示在下面的 table 中。所有实验都是在禁用超线程并启用所有硬件预取器的处理器上执行的。
我测试了以下三种情况:
- 没有初始化循环。也就是说,在上面显示的 "main" 循环之前不会访问缓冲区。我将这种情况称为
NoInit
。本例中只有一个循环。
- 首先使用每个缓存行的加载指令访问缓冲区。一旦所有的线都被触及,主循环就会被执行。我将这种情况称为
LoadInit
。在这种情况下有两个循环。
- 首先使用每个缓存行的一条存储指令访问缓冲区。一旦所有的线都被触及,主循环就会被执行。我将这种情况称为
StoreInit
。在这种情况下有两个循环。
以下 table 显示了英特尔 CFL 处理器上的结果。这些实验是在 Linux 内核版本 4.4.0.
上进行的
以下 table 显示了英特尔 HSW 处理器上的结果。请注意,没有为 HSW 记录事件 L2_RQSTS.PF_HIT
、L2_RQSTS.PF_MISS
和 OFFCORE_REQUESTS.ALL_REQUESTS
。这些实验是在 Linux 内核版本 4.15 上进行的。
每个 table 的第一列包含性能监控事件的名称,其计数显示在其他列中。在列标签中,字母 U
和 K
分别代表用户模式和内核模式事件。对于有两个循环的情况,数字 1 和 2 分别用于指代初始化循环和主循环。例如,LoadInit-1K
表示 LoadInit
情况下初始化循环的内核模式计数。
table 中显示的值已根据缓存行数进行了标准化。它们也按如下颜色编码。绿色越深,相对于同一 table 中所有其他单元格的值就越大。但是,CFL table 的最后三行和 HSW table 的最后两行没有颜色编码,因为这些行中的某些值太大了。这些行涂成深灰色,表示它们不像其他行那样用颜色编码。
我希望用户模式 L2_RQSTS.ALL_RFO
事件的数量等于访问的高速缓存行的数量(即标准化值 1)。这个事件在手册中是这样描述的:
Counts the total number of RFO (read for ownership) requests to L2
cache. L2 RFO requests include both L1D demand RFO misses as well as
L1D RFO prefetches.
它说 L2_RQSTS.ALL_RFO
不仅可以计算来自 L1D 的需求 RFO 请求,还可以计算 L1D RFO 预取。但是,我观察到事件计数不受 L1D 预取器在两个处理器上是启用还是禁用的影响。但即使 L1D 预取器可能生成 RFO 预取,事件计数也应该至少与访问的缓存行数一样大。两个table都可以看出,只有StoreInit-2U
是这样的。同样的观察结果适用于 table 中显示的所有事件。
但是,事件的内核模式计数大约等于用户模式计数的预期值。这与按预期工作的 MEM_INST_RETIRED.ALL_STORES
(或 HSW 上的 MEM_UOPS_RETIRED.ALL_STORES
)形成对比。
由于PMU计数器寄存器的数量有限,我不得不将所有实验分成四个部分。特别是,内核模式计数是从与用户模式计数不同的运行中产生的。相同的内容并不重要。我认为告诉您这一点很重要,因为这解释了为什么某些用户模式计数比相同事件的内核模式计数稍大。
以深灰色显示的事件似乎多算了。第 4 代和第 8 代英特尔处理器规格手册确实提到(分别是 HSD61 和 111 问题)OFFCORE_REQUESTS_OUTSTANDING.DEMAND_RFO
可能多算。但这些结果表明它可能被高估了很多次,而不仅仅是几个事件。
还有其他有趣的观察结果,但它们与问题无关,即:为什么 RFO 计数不符合预期?
您没有标记您的 OS,但我们假设您使用的是 Linux。这些东西在另一个 OS 上会有所不同(甚至可能在相同 OS 的各种变体中)。
在对未映射页面进行读取访问时,内核页面错误处理程序映射到系统范围的共享零页面,具有只读权限。
这对列 LoadInit-1U|K
进行了解释:即使您的初始负载跨过 64 MB 执行负载的 虚拟 区域,只有一个 物理区域 映射了 4K 填充零的页面,因此在第一个 4KB 之后缓存未命中大约为零,在规范化后四舍五入为零。1
在对未映射页面或只读共享零页面进行写访问时,内核将代表进程映射一个新的唯一页面。这个新页面保证被清零,所以除非内核有一些已知为零的页面,否则这涉及在映射之前将页面清零(有效地memset(new_page, 0, 4096)
)。
这在很大程度上解释了 StoreInit-2U|K
以外的其余列。在那些情况下,即使看起来用户程序正在做所有的存储,但内核最终会完成所有艰苦的工作(每页一个存储除外),因为当用户进程在每一页中出错时,内核会写入零对它来说,这具有将所有页面带入 L1 缓存的副作用。当错误处理程序 returns 时,该页面的触发存储和所有后续存储将命中 L1 缓存。
它仍然没有完全解释 StoreInit-2。正如评论中所阐明的那样,K 列实际上包括用户计数,这解释了该列(如预期的那样,减去用户计数使其在每个事件中大致为零)。剩下的困惑是为什么 L2_RQSTS.ALL_RFO
不是 1 而是一些更小的值,如 0.53 或 0.68。也许事件计数不足,或者我们遗漏了一些微架构效果,比如一种防止 RFO 的预取(例如,如果在存储之前通过某种类型的加载操作将行加载到 L1 , RFO 不会发生)。您可以尝试包括其他 L2_RQSTS
事件以查看丢失的事件是否显示在那里。
变化
不需要在所有系统上都这样。当然其他 OSes 可能有不同的策略,但即使 Linux 在 x86 上也可能根据各种因素表现不同。
例如,您可能会分配到 2 MiB huge zero page,而不是 4K 零页。这将改变基准,因为 2 MiB 不适合 L1,因此 LoadInit 测试可能会在第一次和第二次循环中显示 user-space 中的未命中。
更一般地说,如果您使用大页面,页面错误粒度将从 4 KiB 更改为 2 MiB,这意味着只有一小部分归零页面会保留在 L1 和 L2 中,因此您如您所料,得到 L1 和 L2 未命中。如果您的内核 ever implements fault-around 用于匿名映射(或您正在使用的任何映射),它可能会产生类似的效果。
另一种可能性是内核可能会在后台将页面归零,因此准备好零页面。这将从测试中删除 K 计数,因为在页面错误期间不会发生归零,并且可能会将预期的未命中添加到用户计数中。我不确定 Linux 内核是否曾经这样做过或可以选择这样做,但有 patches floating around。其他 OSes 像 BSD 已经做到了。
RFO 预取器
关于“RFO 预取器”- RFO 预取器并不是通常意义上的真正预取器,它们与 L1D 预取器无关,可以将其关闭。据我所知,来自 L1D 的“RFO 预取”只是指为 (a) 商店在计算其地址时(即,当商店数据 uop 执行时)发送 RFO 请求,但在它退休之前或 (b)对于存储缓冲区中接近但尚未到达存储缓冲区头部的存储。
显然,当一个存储到达缓冲区的头部时,就该发送一个 RFO,你不会称其为预取——但为什么不发送一些请求从头部开始的第二个存储等等(情况 b)?或者为什么不在知道存储地址后立即检查 L1D(就像加载一样)然后在未命中时发出推测性 RFO 预取?这些可能被称为 RFO 预取,但它们与普通预取的不同之处在于内核 知道 已请求的地址:这不是猜测。
是推测,如果另一个内核在内核有机会之前为该行发送 RFO,则获取当前头部以外的其他行可能会浪费工作从中写入:请求在那种情况下是无用的,只是增加了一致性流量。因此,如果它经常失败,则有预测器可能会减少此存储缓冲区预取。从某种意义上说,也可能有人猜测存储缓冲区预取可能会发送对尚未退休的初级存储的请求,如果存储最终处于错误的路径上,则以无用的请求为代价。我实际上不确定当前的实现是否这样做。
1 这种行为实际上取决于 L1 缓存的细节:当前的英特尔 VIPT 实现允许同一行的多个虚拟别名在 L1 中愉快地生活。当前的 AMD Zen 实现使用不同的实现(微标签),它不允许 L1 在逻辑上包含多个虚拟别名,所以我预计 Zen 在这种情况下会错过 L2。
总结
考虑以下循环:
loop:
movl [=10=]x1,(%rax)
add [=10=]x40,%rax
cmp %rdx,%rax
jne loop
其中 rax
被初始化为大于 L3 缓存大小的缓冲区地址。每次迭代都对下一个缓存行执行存储操作。我希望从 L1D 发送到 L2 的 RFO 请求数或多或少等于访问的缓存行数。问题是,这似乎只是当我计算内核模式事件时的情况,即使程序在用户模式下运行,除了我在下面讨论的一种情况。缓冲区的分配方式似乎并不重要(.bss、.data 或来自堆)。
详情
我的实验结果显示在下面的 table 中。所有实验都是在禁用超线程并启用所有硬件预取器的处理器上执行的。
我测试了以下三种情况:
- 没有初始化循环。也就是说,在上面显示的 "main" 循环之前不会访问缓冲区。我将这种情况称为
NoInit
。本例中只有一个循环。 - 首先使用每个缓存行的加载指令访问缓冲区。一旦所有的线都被触及,主循环就会被执行。我将这种情况称为
LoadInit
。在这种情况下有两个循环。 - 首先使用每个缓存行的一条存储指令访问缓冲区。一旦所有的线都被触及,主循环就会被执行。我将这种情况称为
StoreInit
。在这种情况下有两个循环。
以下 table 显示了英特尔 CFL 处理器上的结果。这些实验是在 Linux 内核版本 4.4.0.
上进行的以下 table 显示了英特尔 HSW 处理器上的结果。请注意,没有为 HSW 记录事件 L2_RQSTS.PF_HIT
、L2_RQSTS.PF_MISS
和 OFFCORE_REQUESTS.ALL_REQUESTS
。这些实验是在 Linux 内核版本 4.15 上进行的。
每个 table 的第一列包含性能监控事件的名称,其计数显示在其他列中。在列标签中,字母 U
和 K
分别代表用户模式和内核模式事件。对于有两个循环的情况,数字 1 和 2 分别用于指代初始化循环和主循环。例如,LoadInit-1K
表示 LoadInit
情况下初始化循环的内核模式计数。
table 中显示的值已根据缓存行数进行了标准化。它们也按如下颜色编码。绿色越深,相对于同一 table 中所有其他单元格的值就越大。但是,CFL table 的最后三行和 HSW table 的最后两行没有颜色编码,因为这些行中的某些值太大了。这些行涂成深灰色,表示它们不像其他行那样用颜色编码。
我希望用户模式 L2_RQSTS.ALL_RFO
事件的数量等于访问的高速缓存行的数量(即标准化值 1)。这个事件在手册中是这样描述的:
Counts the total number of RFO (read for ownership) requests to L2 cache. L2 RFO requests include both L1D demand RFO misses as well as L1D RFO prefetches.
它说 L2_RQSTS.ALL_RFO
不仅可以计算来自 L1D 的需求 RFO 请求,还可以计算 L1D RFO 预取。但是,我观察到事件计数不受 L1D 预取器在两个处理器上是启用还是禁用的影响。但即使 L1D 预取器可能生成 RFO 预取,事件计数也应该至少与访问的缓存行数一样大。两个table都可以看出,只有StoreInit-2U
是这样的。同样的观察结果适用于 table 中显示的所有事件。
但是,事件的内核模式计数大约等于用户模式计数的预期值。这与按预期工作的 MEM_INST_RETIRED.ALL_STORES
(或 HSW 上的 MEM_UOPS_RETIRED.ALL_STORES
)形成对比。
由于PMU计数器寄存器的数量有限,我不得不将所有实验分成四个部分。特别是,内核模式计数是从与用户模式计数不同的运行中产生的。相同的内容并不重要。我认为告诉您这一点很重要,因为这解释了为什么某些用户模式计数比相同事件的内核模式计数稍大。
以深灰色显示的事件似乎多算了。第 4 代和第 8 代英特尔处理器规格手册确实提到(分别是 HSD61 和 111 问题)OFFCORE_REQUESTS_OUTSTANDING.DEMAND_RFO
可能多算。但这些结果表明它可能被高估了很多次,而不仅仅是几个事件。
还有其他有趣的观察结果,但它们与问题无关,即:为什么 RFO 计数不符合预期?
您没有标记您的 OS,但我们假设您使用的是 Linux。这些东西在另一个 OS 上会有所不同(甚至可能在相同 OS 的各种变体中)。
在对未映射页面进行读取访问时,内核页面错误处理程序映射到系统范围的共享零页面,具有只读权限。
这对列 LoadInit-1U|K
进行了解释:即使您的初始负载跨过 64 MB 执行负载的 虚拟 区域,只有一个 物理区域 映射了 4K 填充零的页面,因此在第一个 4KB 之后缓存未命中大约为零,在规范化后四舍五入为零。1
在对未映射页面或只读共享零页面进行写访问时,内核将代表进程映射一个新的唯一页面。这个新页面保证被清零,所以除非内核有一些已知为零的页面,否则这涉及在映射之前将页面清零(有效地memset(new_page, 0, 4096)
)。
这在很大程度上解释了 StoreInit-2U|K
以外的其余列。在那些情况下,即使看起来用户程序正在做所有的存储,但内核最终会完成所有艰苦的工作(每页一个存储除外),因为当用户进程在每一页中出错时,内核会写入零对它来说,这具有将所有页面带入 L1 缓存的副作用。当错误处理程序 returns 时,该页面的触发存储和所有后续存储将命中 L1 缓存。
它仍然没有完全解释 StoreInit-2。正如评论中所阐明的那样,K 列实际上包括用户计数,这解释了该列(如预期的那样,减去用户计数使其在每个事件中大致为零)。剩下的困惑是为什么 L2_RQSTS.ALL_RFO
不是 1 而是一些更小的值,如 0.53 或 0.68。也许事件计数不足,或者我们遗漏了一些微架构效果,比如一种防止 RFO 的预取(例如,如果在存储之前通过某种类型的加载操作将行加载到 L1 , RFO 不会发生)。您可以尝试包括其他 L2_RQSTS
事件以查看丢失的事件是否显示在那里。
变化
不需要在所有系统上都这样。当然其他 OSes 可能有不同的策略,但即使 Linux 在 x86 上也可能根据各种因素表现不同。
例如,您可能会分配到 2 MiB huge zero page,而不是 4K 零页。这将改变基准,因为 2 MiB 不适合 L1,因此 LoadInit 测试可能会在第一次和第二次循环中显示 user-space 中的未命中。
更一般地说,如果您使用大页面,页面错误粒度将从 4 KiB 更改为 2 MiB,这意味着只有一小部分归零页面会保留在 L1 和 L2 中,因此您如您所料,得到 L1 和 L2 未命中。如果您的内核 ever implements fault-around 用于匿名映射(或您正在使用的任何映射),它可能会产生类似的效果。
另一种可能性是内核可能会在后台将页面归零,因此准备好零页面。这将从测试中删除 K 计数,因为在页面错误期间不会发生归零,并且可能会将预期的未命中添加到用户计数中。我不确定 Linux 内核是否曾经这样做过或可以选择这样做,但有 patches floating around。其他 OSes 像 BSD 已经做到了。
RFO 预取器
关于“RFO 预取器”- RFO 预取器并不是通常意义上的真正预取器,它们与 L1D 预取器无关,可以将其关闭。据我所知,来自 L1D 的“RFO 预取”只是指为 (a) 商店在计算其地址时(即,当商店数据 uop 执行时)发送 RFO 请求,但在它退休之前或 (b)对于存储缓冲区中接近但尚未到达存储缓冲区头部的存储。
显然,当一个存储到达缓冲区的头部时,就该发送一个 RFO,你不会称其为预取——但为什么不发送一些请求从头部开始的第二个存储等等(情况 b)?或者为什么不在知道存储地址后立即检查 L1D(就像加载一样)然后在未命中时发出推测性 RFO 预取?这些可能被称为 RFO 预取,但它们与普通预取的不同之处在于内核 知道 已请求的地址:这不是猜测。
是推测,如果另一个内核在内核有机会之前为该行发送 RFO,则获取当前头部以外的其他行可能会浪费工作从中写入:请求在那种情况下是无用的,只是增加了一致性流量。因此,如果它经常失败,则有预测器可能会减少此存储缓冲区预取。从某种意义上说,也可能有人猜测存储缓冲区预取可能会发送对尚未退休的初级存储的请求,如果存储最终处于错误的路径上,则以无用的请求为代价。我实际上不确定当前的实现是否这样做。
1 这种行为实际上取决于 L1 缓存的细节:当前的英特尔 VIPT 实现允许同一行的多个虚拟别名在 L1 中愉快地生活。当前的 AMD Zen 实现使用不同的实现(微标签),它不允许 L1 在逻辑上包含多个虚拟别名,所以我预计 Zen 在这种情况下会错过 L2。