Linux perf 报告缓存未命中意外指令

Linux perf reporting cache misses for unexpected instruction

我正在尝试将一些性能工程技术应用到 Dijkstra 算法的实现中。为了找到(原始和未优化的)程序中的瓶颈,我使用 perf 命令来记录缓存未命中的次数。相关的代码片段如下,它找到距离最小的未访问节点:

for (int i = 0; i < count; i++) {
    if (!visited[i]) {
        if (tmp == -1 || dist[i] < dist[tmp]) {
            tmp = i;
        }
    }
}

对于 LLC-load-misses 指标,perf report 显示程序集的以下注释:

       │             for (int i = 0; i < count; i++) {                                                                                                                           ▒
  1.19 │ ff:   add    [=12=]x1,%eax                                                                                                                                                  ▒
  0.03 │102:   cmp    0x20(%rsp),%eax                                                                                                                                            ▒
       │     ↓ jge    135                                                                                                                                                        ▒
       │                 if (!visited[i]) {                                                                                                                                      ▒
  0.07 │       movslq %eax,%rdx                                                                                                                                                  ▒
       │       mov    0x18(%rsp),%rdi                                                                                                                                            ◆
  0.70 │       cmpb   [=12=]x0,(%rdi,%rdx,1)                                                                                                                                         ▒
  0.53 │     ↑ jne    ff                                                                                                                                                         ▒
       │                     if (tmp == -1 || dist[i] < dist[tmp]) {                                                                                                             ▒
  0.07 │       cmp    [=12=]xffffffff,%r13d                                                                                                                                          ▒
       │     ↑ je     fc                                                                                                                                                         ▒
  0.96 │       mov    0x40(%rsp),%rcx                                                                                                                                            ▒
  0.08 │       movslq %r13d,%rsi                                                                                                                                                 ▒
       │       movsd  (%rcx,%rsi,8),%xmm0                                                                                                                                        ▒
  0.13 │       ucomis (%rcx,%rdx,8),%xmm0                                                                                                                                        ▒
 57.99 │     ↑ jbe    ff                                                                                                                                                         ▒
       │                         tmp = i;                                                                                                                                        ▒
       │       mov    %eax,%r13d                                                                                                                                                 ▒
       │     ↑ jmp    ff                                                                                                                                                         ▒
       │                     }                                                                                                                                                   ▒
       │                 }                                                                                                                                                       ▒
       │             }   

那么我的问题是:为什么 jbe 指令会产生如此多的缓存未命中?如果我没记错的话,这条指令根本不需要从内存中检索任何东西。我认为这可能与指令缓存未命中有关,但即使使用 L1-dcache-load-misses 仅测量 L1 数据缓存未命中也表明该指令中有很多缓存未命中。

这让我有点难过。谁能解释这个(在我眼中)奇怪的结果?提前谢谢你。

关于你的例子:

在高位计数器之前和处有几条指令:

        │       movsd  (%rcx,%rsi,8),%xmm0
   0.13 │       ucomis (%rcx,%rdx,8),%xmm0
  57.99 │     ↑ jbe    ff

"movsd" 将 (%rcx,%rsi,8) 中的字(某些数组访问)加载到 xmm0 寄存器中,并且 "ucomis" 从 (%rcx,%rdx,8) 中加载另一个字并将其与刚刚加载的值进行比较xmm0 寄存器。 "jbe"是条件跳转,取决于比较结果。

许多现代 Intel CPUs(可能还有 AMD)可以并且将会融合(组合)一些运算组合(realworldtech.com/nehalem/5 "into a single uop, CMP+JCC"),以及 cmp +条件跳转非常常见的指令组合被融合(你可以用Intel IACA模拟工具检查它,你的CPU使用ver 2.1)。融合对可能会在 perf/PMUs/PEBS 中错误地报告,大多数事件偏向于两条指令之一。

这段代码可能意味着表达式 "dist[i] < dist[tmp]" 产生了两次内存访问,并且这两个值都用于 ucomis 指令中,该指令(部分?)与 jbe 条件跳转融合。 dist[i] 或 dist[tmp] 或两个表达式都会产生大量未命中。任何此类未命中都将阻止 ucomis 生成结果,并阻止 jbe 给出下一条要执行的指令(或退出预测的指令)。因此,jbe 可能会以高计数器而不是真正的内存访问指令而闻名(并且对于 "far" 缓存响应之类的事件,有一些偏向最后阻塞的指令)。

您可以尝试将 visited[N] 和 dist[N] 数组合并到 struct { int visited; float dist} 的数组 [N] 中,以在您访问 array[i].visited 时强制预取 array[i].dist 或者您可能会尝试更改顶点访问顺序,或重新编号图形顶点,或对下一个或多个元素进行一些软件预取 (?)


关于通用 perf 事件名称问题和可能的非核心偏差。

perf (perf_events) Linux 中的工具在调用 perf list 时使用预定义的事件集,一些列出的硬件事件无法实现;其他映射到当前 CPU 功能(有些映射不完全正确)。关于真实 PMU 的一些基本信息在你的 https://software.intel.com/sites/products/collateral/hpc/vtune/performance_analysis_guide.pdf 中(但它有更多关于相关 Nehalem-EP 变体的详细信息)。

对于您的 Nehalem(Intel Core i5 750,具有 8MB 的 L3 高速缓存且没有 multi-CPU/multi-socket/NUMA 支持)perf 会将标准 ("Generic cache events") LLC-load-misses 事件映射为 .. "OFFCORE_RESPONSE.ANY_DATA.ANY_LLC_MISS" 写在性能事件映射的最佳文档中(唯一的)-内核源代码

http://elixir.free-electrons.com/linux/v4.8/source/arch/x86/events/intel/core.c#L1103

 u64 nehalem_hw_cache_event_ids ...
[ C(LL  ) ] = {
    [ C(OP_READ) ] = {
        /* OFFCORE_RESPONSE.ANY_DATA.LOCAL_CACHE */
        [ C(RESULT_ACCESS) ] = 0x01b7,
        /* OFFCORE_RESPONSE.ANY_DATA.ANY_LLC_MISS */
        [ C(RESULT_MISS)   ] = 0x01b7,
...
/*
 * Nehalem/Westmere MSR_OFFCORE_RESPONSE bits;
 * See IA32 SDM Vol 3B 30.6.1.3
 */
#define NHM_DMND_DATA_RD    (1 << 0)
#define NHM_DMND_READ       (NHM_DMND_DATA_RD)
#define NHM_L3_MISS (NHM_NON_DRAM|NHM_LOCAL_DRAM|NHM_REMOTE_DRAM|NHM_REMOTE_CACHE_FWD)
...
 u64 nehalem_hw_cache_extra_regs
  ..
 [ C(LL  ) ] = {
    [ C(OP_READ) ] = {
        [ C(RESULT_ACCESS) ] = NHM_DMND_READ|NHM_L3_ACCESS,
        [ C(RESULT_MISS)   ] = NHM_DMND_READ|NHM_L3_MISS,

我认为这个事件不准确:cpu 管道将 post(无序)加载请求到缓存层次结构并将执行其他指令。一段时间后 (around 10 cycles to reach and get response from L2 and 40 cycles to reach L3),对应的(offcore?)PMU 中会出现带有未命中标志的响应,以增加计数器。在此计数器溢出时,将从该 PMU 生成分析中断。在几个 cpu 时钟周期内它将到达流水线以中断它,perf_events 子系统的处理程序将通过注册当前(中断)EIP/RIP 指令指针来处理此问题并将 PMU 计数器重置回某个负值(例如,-100000 表示每计数 100000 次 L3 未命中获得中断;使用 perf record -e LLC-load-misses -c 100000 设置精确计数或 perf 将自动调整限制以获得一些默认频率)。注册的EIP/RIP不是加载命令的IP,也可能不是要使用加载数据的命令的EIP/RIP。

但是如果你的 CPU 是系统中唯一的套接字并且你访问普通内存(不是一些映射的 PCI-express space),L3 miss 实际上将被实现为本地内存访问并且有一些计数器... (https://software.intel.com/en-us/node/596851 - "Any memory requests missing here must be serviced by local or remote DRAM").

您的 CPU 有一些 PMU 事件列表:

应该有一些关于 ANY_LLC_MISS offcore PMU 事件实现和 Nhm 的 PEBS 事件列表的信息,但我现在找不到。

我可以建议您将 https://github.com/andikleen/pmu-tools 中的 ocperf 与 CPU 的任何 PMU 事件一起使用,而无需手动对它们进行编码。您的 CPU 中有一些 PEBS 事件,并且有延迟分析 / perf mem 用于某种内存访问分析(一些随机性能内存 pdf:2012 post "perf: add memory access sampling support",RH 2013 - pg26-30, 2015 年仍未记录 - sowa pg19 ls /sys/devices/cpu/events)。对于较新的 CPUs,有较新的工具,例如 ucevent.

我也可以推荐您尝试使用 kcachegrind GUI 的 valgrind 程序的 cachegrind profiler/cache simulator tool 来查看配置文件。基于 Valgrind 的分析器可以帮助您了解代码如何工作的基本概念:它们收集每条指令的准确指令执行计数,并且 cachegrind 还模拟一些抽象的多级缓存。但是真正的 CPU 每个周期会执行几条指令(因此,callgrind/cachegrind 1 条指令的成本模型 = 1 cpu 时钟周期给出了一些错误;cachegrind 缓存模型没有与真实缓存相同的逻辑)。所有 valgrind 工具都是动态二进制检测工具,与原生 运行.

相比,它们会使您的程序速度降低 20-30 倍

当您读取一个内存位置时,处理器将尝试预取相邻的内存位置并缓存它们。

如果您正在读取一个对象数组,这些对象都分配在内存中的连续块中,那么效果很好。

但是,例如,如果您有一个位于堆中的指针数组,除非您使用某种专门为此设计的自定义分配器,否则您不太可能迭代内存的连续部分。

正因为如此,取消引用应该被视为某种成本。结构数组比结构指针数组更有效。

Herb Sutter(C++ 委员会成员)在本次演讲中谈到了这一点 https://youtu.be/TJHgp1ugKGM?t=21m31s