【问题标题】:Linux perf reporting cache misses for unexpected instructionLinux perf 报告意外指令的缓存未命中
【发布时间】:2017-10-03 07:40:09
【问题描述】:

我正在尝试将一些性能工程技术应用于 Dijkstra 算法的实现。为了找到(幼稚且未优化的)程序中的瓶颈,我使用perf 命令记录缓存未命中的数量。相关代码的sn-p如下,找到距离最小的未访问节点:

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    $0x1,%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   $0x0,(%rdi,%rdx,1)                                                                                                                                         ▒
  0.53 │     ↑ jne    ff                                                                                                                                                         ▒
       │                     if (tmp == -1 || dist[i] < dist[tmp]) {                                                                                                             ▒
  0.07 │       cmp    $0xffffffff,%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 数据缓存未命中也表明该指令中有很多缓存未命中。

这有点难倒我。谁能解释这个(在我看来)奇怪的结果?提前谢谢你。

【问题讨论】:

  • Stephen,您的确切 CPU 型号是什么(查找微架构名称和可用 PMU 事件集)。 LLC-load-misses 是合成性能事件,它映射到不同的硬件 PMU 事件(另请查看 pref record ... -vvv ./program),其中一些是准确的,而另一些则不准确。对于不精确的事件,会报告错误的指令地址,有时偏差很小,有时偏差很大(即使对于不同的功能)。英特尔中有十个确切的事件“PEBS” - 试试dmesg | grep -i pebs。尝试 Intel 的 github.com/andikleen/pmu-tools 中的 ocperf 工具来使用真实的硬件事件
  • 感谢您的回复,osgx。我运行它的 CPU 是 Intel Core i5 750 (Nehalem)。我不知道LLC-load-misses 是一个不精确的事件,并假设它是精确的,因为perf list 将其列为硬件缓存事件。如果我理解正确,我需要使用 rXXXXXXX 原始硬件事件,但是我不知道如何从我在这里打开的 Nehalem 性能工程手册中推断出确切的事件名称:software.intel.com/sites/products/collateral/hpc/vtune/…
  • Stephen,并非所有硬件事件都是准确的。使用 ocperf 获取 intel 事件名称,无需手动转换为 rXXXX 原始代码。
  • Stephen,您的 linux 内核版本(或 linux 发行版)是什么?尝试valgrind 程序和kcachegrind GUI 的cachegrind profiler/cache 模拟器工具,以获取有关代码如何工作的基本概念(每个instr 的确切指令执行计数),热路径在哪里以及缓存密集型代码在哪里.但请注意,它只是模拟器,其结果不等于真正的 cpu 运行和真正的缓存方法;而且它非常慢 - 与原生相比,在 valgrind 下运行速度预计会慢 20-30 倍。
  • PS:高位计数器附近有两条指令:"ucomis (%rcx,%rdx,8),%xmm0 + ↑ jbe",其中ucomis为some kind of compare,带内存参数。 Intel CPU 可以将一些操作组合(realworldtech.com/nehalem/5“成单个 uop,CMP+JCC”)融合在一起,而 cmp + 条件跳转是要融合的常见指令(您可以使用 Intel IACA simulating tool ver 2.1 检查它)。融合对通常在 perf 中针对两条指令中的一个 IP 报告。

标签: linux performance caching perf


【解决方案1】:

关于你的例子:

高位计数器前后有几条指令:

        │       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 CPU(可能还有 AMD)可以并且将融合(组合)一些操作组合(realworldtech.com/nehalem/5“到单个 uop,CMP+JCC”),以及 cmp + 条件跳转要融合的非常常见的指令组合(您可以使用Intel IACA 模拟工具检查它,为您的CPU 使用ver 2.1)。在 perf/PMU/PEBS 中可能会错误地报告融合对,并且大多数事件会偏向两个指令之一。

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

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


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

Linux 中的perf (perf_events) 工具在调用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 缓存,不支持多 CPU/多插槽/NUMA),perf 会将标准 ("Generic cache events") LLC-load-misses 事件映射为 .. "OFFCORE_RESPONSE.ANY_DATA. ANY_LLC_MISS”写在perf event mappings的最佳文档中(唯一)——内核源码

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 管道将向缓存层次结构发布(无序)加载请求,并将执行其他指令。一段时间后(around 10 cycles 到达并从 L2 和 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 空间),L3 未命中实际上将被实现为本地内存访问,并且有一些计数器用于此... (https://software.intel.com/en-us/node/596851 - “这里丢失的任何内存请求都必须由本地或远程 DRAM 提供服务”)。

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

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

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

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

【讨论】:

  • 感谢您的详尽解释。在理解这些事件的运作方式方面,您肯定对我有很大帮助。
  • 感谢您的出色回答,您确实是性能和低级编程方面的专家,您能否在这里看看我的类似问题:stackoverflow.com/questions/63990981/…
【解决方案2】:

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 2013-01-18
    • 2014-07-28
    • 2019-07-28
    • 2017-07-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-27
    • 2017-04-19
    相关资源
    最近更新 更多