【问题标题】:Can perf account for all cache misses?perf 可以解释所有缓存未命中吗?
【发布时间】:2015-07-05 02:07:01
【问题描述】:

我正在尝试了解 perf 记录的缓存未命中。我有一个最小的程序:

int main(void)
{
    return 0;
}

如果我编译这个:

gcc -std=c99 -W -Wall -Werror -O3 -S -o test.S test.c

我得到了一个预期的小程序:

        .file   "test.c"
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        xorl    %eax, %eax
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Debian 4.7.2-5) 4.7.2"
        .section        .note.GNU-stack,"",@progbits

只有两条指令,xorlret,程序的大小应该小于缓存行,所以我希望如果我运行perf -e "cache-misses:u" ./test,我应该只会看到一个缓存未命中。但是,我看到的是 2 到 ~400 之间。同样,perf -e "cache-misses" ./test 的结果是 ~700 到 ~2500。

这仅仅是一个完美估计计数的情况,还是缓存未命中的发生方式使得对它们的推理变得近似?例如,如果我生成然后读取内存中的整数数组,我可以推断预取(顺序访问应该允许完美的预取)还是有其他东西在起作用?

【问题讨论】:

  • 您创建了main 而不是_start,并且可能将其构建到动态链接的可执行文件中!所以有所有的 CRT 启动代码、初始化 libc 和几个系统调用。运行strace ./test。更有趣的是一个静态链接的可执行文件,它只使用syscall 指令从_start 入口点进行_exit(0) 系统调用。

标签: performance caching performancecounter perf


【解决方案1】:

您创建了main 而不是_start,并且可能将其构建到动态链接的可执行文件中!所以有所有的 CRT 启动代码、初始化 libc 和几个系统调用。运行strace ./test 并查看它发出了多少系统调用。 (当然,用户空间中有很多工作不涉及系统调用)。

更有趣的是静态链接的可执行文件,它只使用syscall 指令从_start 入口点进行_exit(0)exit_group(0) 系统调用。

给定一个包含这些内容的exit.s

mov $231, %eax
syscall

将其构建为静态可执行文件,因此这两条指令是唯一在用户空间中执行的指令:

$ gcc -static -nostdlib exit.s
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
  # the default is fine, our instructions are at the start of the .text section

$ perf stat -e cache-misses:u ./a.out 

 Performance counter stats for './a.out':

                 6      cache-misses:u                                              

       0.000345362 seconds time elapsed

       0.000382000 seconds user
       0.000000000 seconds sys

我告诉它计算 cache-misses:u 以仅测量 用户空间 缓存未命中,而不是进程正在运行的核心上的所有内容。 (这将包括在进入用户空间之前和处理 exit_group() 系统调用时内核缓存未命中。以及潜在的中断处理程序。

(当特权级别是用户、内核或两者时,PMU 中有硬件支持对事件进行计数。所以我们应该期望计数在从内核转换期间完成的计数中最多减少 1 或 2 ->user 或 user->kernel。(更改 CS,可能导致从 GDT 加载由新 CS 值索引的段描述符)。


但是cache-misses 到底算什么事件呢?

How does Linux perf calculate the cache-references and cache-misses events 解释:

perf 显然将 cache-misses 映射到计算 last-level 缓存未命中的硬件事件。所以它类似于 DRAM 访问次数。

多次尝试访问 L1d 或 L1i 缓存中的同一行,而 L1 未命中已经未完成,只会添加另一个等待同一传入缓存行的内容。所以它不计算必须等待缓存的加载(或代码获取)。 多个负载可以合并为一个访问。

但也请记住,代码获取需要通过 iTLB,触发页面遍历。 页面遍历加载被缓存,即它们是通过缓存层次结构获取的。因此,如果他们确实错过了,他们会被 cache-misses 事件计算在内。

程序的重复运行可能导致0 cache-miss 事件。 可执行二进制文件是一个文件,该文件由页面缓存缓存(操作系统的磁盘缓存)。该物理内存被映射到运行它的进程的地址空间。它当然可以在进程启动/停止期间在 L3 中保持热状态。更有趣的是,显然页表也很热。 (不是字面上的“保持”热度;我假设内核每次都必须编写一个新的。但大概页面遍历器至少在 L3 缓存中命中。)

或者至少其他任何导致“额外”cache-miss 事件的原因都不必发生。

我使用perf stat -r16 运行了 16 次并显示均值 +stddev

$ perf stat -e instructions:u,L1-dcache-loads:u,L1-dcache-load-misses:u,cache-misses:u,itlb_misses.walk_completed:u -r 16 ./exit

 Performance counter stats for './exit' (16 runs):

                 3      instructions:u                                              
                 1      L1-dcache-loads                                             
                 5      L1-dcache-load-misses     #  506.25% of all L1-dcache hits    ( +-  6.37% )
                 1      cache-misses:u                                                ( +-100.00% )
                 2      itlb_misses.walk_completed:u                                   

         0.0001422 +- 0.0000108 seconds time elapsed  ( +-  7.57% )

注意缓存未命中的 +-100%。

我不知道为什么我们有 2 个 itlb_misses.walk_completed 事件,而不仅仅是 1 个。计数itlb_misses.miss_causes_a_walk:u 反而给我们4 始终如一。

减少到-r 1 并使用手动向上箭头重复运行,cache-misses 在 3 到 13 之间反弹。系统大部分时间处于空闲状态,但有一点后台网络流量。

我也不知道为什么任何东西都显示为 L1D 加载,或者为什么一次加载会出现 6 次未命中。但 Hadi 的回答说 perf 的 L1-dcache-load-misses 事件实际上算上 L1D.REPLACEMENT,所以页面浏览可以解释这一点。而L1-dcache-loads 计数MEM_INST_RETIRED.ALL_LOADSmov-immediate 不是负担,我也不会想到 syscall 也是。但也许是这样,否则硬件会错误地计算内核指令或者某处有一个 off-by-1。

【讨论】:

    【解决方案2】:

    这不是一个简单的话题,但如果您有兴趣计算(例如)访问数组时的缓存未命中数,那么您应该从这开始。

    有许多陷阱,但最简单的方法可能会导致洞察力,从分配数组的程序开始,将值存储到数组中,然后以可编程的次数读取数组。

    将值存储到数组中是创建虚拟到物理页面映射所必需的。由于操作系统在初始化这些页面时使用的技巧,本节的性能计数器结果可能难以理解——例如,从映射到零填充页面开始并将访问权限设置为“写入时复制”。

    页面实例化后,读取的性能计数可能更有意义。我使用可编程的读取次数,以便可以获取 20 次读取和 10 次读取的计数器值之间的差异(例如)。 数组大小应选择显着大于您要测试的级别的可用缓存。

    不幸的是,“性能”使得在硬件级别(这是唯一重要的级别!)计算性能计数器中实际编程的内容变得相对困难。事件越“通用”,就越难猜测实际测量的是什么……在我最近基于 Intel 的系统上,“性能列表”给出了可用事件的长列表(>3600 行)。从标记为“缓存:”的部分开始的事件是英特尔架构软件开发人员手册第 3 卷第 19 章中描述的硬件事件的直接翻译。

    您担心硬件预取的计数方式是正确的。在最近的英特尔架构中,报告缓存访问的事件通常可以配置为计算需求访问、硬件预取或两者兼有。报告加载源位置的事件指令不会提供任何关于硬件预取在哪里找到数据的洞察力——只有在加载操作执行时它距离处理器有多近。 p>

    我发现事件“l1d.replacements”是最新英特尔处理器上可靠的 L1 数据缓存未命中指示器。它只计算所有移入 L1 数据缓存的缓存行(无论是由于加载、存储、预取等)。在层次结构的另一端,DRAM 计数器(例如,“uncore_imc_0/cas_count_read/”)也是可靠的,但由于系统中的任何其他活动而受到污染。 “双面”缓存(例如,L2 和 L3)的计数器更容易令人困惑,因为并不总是清楚事件是计算从一侧还是另一侧或两者发送的缓存行(例如,“l2_lines_in.全部”)。通过一些仔细控制的实验,通常可以在这些中间级别找到可靠且可理解的事件子集。并非总是能够找到足够可靠的计数器来全面计算内存层次结构的每个级别的所有流量,但这是一个更长的故事......

    【讨论】:

      【解决方案3】:

      进程内存空间不仅仅与你的代码有关,还有堆、栈、数据段等不同的来源也会导致缓存未命中。


      (来源:tenouk.com

      我认为您无法估计缓存未命中数,就像您无法预测多线程程序中每个线程的运行顺序一样。

      但是,缓存未命中分析对于找出和定位false sharing 很有用。以下是一些您可以参考的有用链接:

      1. http://igoro.com/archive/gallery-of-processor-cache-effects/
      2. http://qqibrow.github.io/CPU-Cache-Effects-and-Linux-Perf/

      【讨论】:

      • 这是一个单线程程序;没有任何东西的共享。如果他们实际上构建了一个刚刚进行退出系统调用的静态可执行文件,那么 OP 对大约 1 次缓存未命中的猜测是合理的。但相反,他们仍然拥有初始化 libc 等的 CRT 代码。
      猜你喜欢
      • 1970-01-01
      • 2013-01-18
      • 2017-04-19
      • 2019-07-28
      • 2017-10-03
      • 2014-06-29
      • 2016-12-09
      • 1970-01-01
      • 2011-01-23
      相关资源
      最近更新 更多