【问题标题】:Profiling Cache hit rate of a function of C program分析C程序一个函数的缓存命中率
【发布时间】:2021-02-11 14:28:01
【问题描述】:

我想获取在 Linux 机器上运行的 C/C++ 程序 (foo) 的特定功能的缓存命中率。我正在使用 gcc 并且没有编译器优化。使用 perf 我可以使用以下命令获取整个程序的命中率。

perf stat -e L1-dcache-loads,L1-dcache-load-misses,L1-dcache-stores,L1-dcache-store-misses ./a.out

但我只对内核 foo 感兴趣。

有没有办法使用 perf 或任何其他工具仅获得 foo 的命中率?

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>


#define NI 192
#define NJ NI

#ifndef DATA_TYPE
    #define DATA_TYPE float
#endif


static 
void* xmalloc(size_t num)
{
    void * nnew = NULL;
    int ret = posix_memalign (&nnew, 32, num);
    if(!nnew || ret)
    {
        fprintf(stderr, "Can not allocate Memory\n");
        exit(1);
    }
    return nnew;
}

void* alloc_data(unsigned long long int n, int elt_size)
{
    size_t val = n;
    val *= elt_size;
    void* ret = xmalloc(val);
    return ret;
}


/* Array initialization. */
static
void init_array(int ni, int nj,
        DATA_TYPE A[NI][NJ],
        DATA_TYPE R[NJ][NJ],
        DATA_TYPE Q[NI][NJ])
{
  int i, j;

  for (i = 0; i < ni; i++)
    for (j = 0; j < nj; j++) {
      A[i][j] = ((DATA_TYPE) i*j) / ni;
      Q[i][j] = ((DATA_TYPE) i*(j+1)) / nj;
    }
  for (i = 0; i < nj; i++)
    for (j = 0; j < nj; j++)
      R[i][j] = ((DATA_TYPE) i*(j+2)) / nj;
}


/* Main computational kernel.*/

static
void foo(int ni, int nj,
        DATA_TYPE A[NI][NJ],
        DATA_TYPE R[NJ][NJ],
        DATA_TYPE Q[NI][NJ])
{
  int i, j, k;

  DATA_TYPE nrm;
  for (k = 0; k < nj; k++)
  {
    nrm = 0;
    for (i = 0; i < ni; i++)
      nrm += A[i][k] * A[i][k];
    R[k][k] = sqrt(nrm);
    for (i = 0; i < ni; i++)
      Q[i][k] = A[i][k] / R[k][k];
    for (j = k + 1; j < nj; j++)
    {
      R[k][j] = 0;
      for (i = 0; i < ni; i++)
        R[k][j] += Q[i][k] * A[i][j];
      for (i = 0; i < ni; i++)
        A[i][j] = A[i][j] - Q[i][k] * R[k][j];
    }
  }
}


int main(int argc, char** argv)
{
  /* Retrieve problem size. */
  int ni = NI;
  int nj = NJ;

  /* Variable declaration/allocation. */
  DATA_TYPE (*A)[NI][NJ];
  DATA_TYPE (*R)[NI][NJ];
  DATA_TYPE (*Q)[NI][NJ];

  A = ((DATA_TYPE (*)[NI][NJ])(alloc_data((NI*NJ), (sizeof(DATA_TYPE)))));
  R = ((DATA_TYPE (*)[NI][NJ])(alloc_data((NI*NJ), (sizeof(DATA_TYPE)))));
  Q = ((DATA_TYPE (*)[NI][NJ])(alloc_data((NI*NJ), (sizeof(DATA_TYPE)))));
  
/* Initialize array(s). */
  init_array (ni, nj,
          (*A),
          (*R),
          (*Q));


  /* Run kernel. */
  foo (ni, nj, *A, *R, *Q);

  /* Be clean. */
  free((void *)A);
  free((void *)R);
  free((void *)Q);

  return 0;
}

lscpu 命令的输出是:

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                16
On-line CPU(s) list:   0-15 
Thread(s) per core:    2
Core(s) per socket:    8
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel 
CPU family:            6
Model:                 63
Model name:            Intel(R) Core(TM) i7-5960X CPU @ 3.00GHz
Stepping:              2
CPU max MHz:           3500.0000
CPU min MHz:           1200.0000
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              20480K
NUMA node0 CPU(s):     0-15

【问题讨论】:

  • 写一个只运行foo()的程序并测量它?
  • 您想要的是卡尺测量:调用 foo() 之前的“开始计数器”和 foo() 结束时的“停止计数器”。为此,您需要检测代码并重新构建它。获取计数器的能力取决于处理器架构及其 PMU。获取计数器的方法是特定于供应商的。这就是为什么像 papi 这样的库非常有用,因为它们透明地支持多个处理器/PMU 架构。为什么你不能使用 papi ?
  • @hyde:这将包括动态链接和分配/初始化部分的计数。您可以通过使用perf stat --all-user(或使用较旧的perf,使用event:u,event:u,...)仅计算用户空间所以是的,如果您可以重复foo 很多次以淹没背景,那么您可以对整个程序进行计时初始化工作的噪音;如果它可以运行多次而不重做它的初始化。但是,如果您想使用包含大量初始化时间的大型数组运行 foo,这可能是不切实际的。
  • @PeterCordes 可以使用静态链接。可以预先计算数组。
  • 但是当我尝试使用 PAPI_add_event 函数添加这些事件时,这将返回错误代码 -8(事件存在,但由于计数器资源限制而无法计数)。当我尝试添加三个事件时它失败了。如果我只添加两个事件,它工作正常。

标签: c linux perf papi intel-pmu


【解决方案1】:

您也可以使用Likwid 及其Marker-API。它使检测代码的某些区域变得非常容易。您可以在 haswell 架构上使用预定义的性能组 ICACHE 来获取 L1 缓存未命中率,或者为 L1 命中率定义自己的性能组。

#include likwid.h
LIKWID_MARKER_INIT;
LIKWID_MARKER_START("region foo");

foo();

LIKWID_MARKER_STOP("region foo");
LIKWID_MARKER_CLOSE;

运行应用程序:

./likwid-perfctr -g ICACHE -m &lt;your application&gt;

确保使用 -DLIKWID-PERFMON 编译并添加 Likwid 包含和库路径并链接 Likwid 库:-L$LIKWID_LIB -I$LIKWID_INCLUDE -llikwid。 一切都在他们的github wiki上得到了很好的记录

【讨论】:

    【解决方案2】:

    首先,请注意您的处理器不支持L1-dcache-store-missesperf stat 会在输出中告诉你。

    perf stat 不允许您仅分析选定的代码区域。为此,您必须手动检测代码,以便根据需要控制感兴趣区域周围的指定事件。

    如果您的处理器 (Haswell) 上没有多路复用,则无法计算事件 L1-dcache-loadsL1-dcache-load-missesL1-dcache-stores。它们分别映射到本机事件 MEM_UOPS_RETIRED.ALL_LOADSL1D.REPLACEMENTMEM_UOPS_RETIRED.ALL_STORES。这些事件中的每一个只能计数前四个通用计数器。另外,i7-5960X 的规格更新文档中没有记录,但 i7-5960X 中确实存在一个错误(它记录在其他 Haswell 处理器和其他一些微架构处理器的规格更新文档中)。此错误在不同版本的 perf 中处理方式不同。从内核版本 4.1-rc7 开始,如果在逻辑内核上启用了受 bug 影响的事件之一,并且如果在引导时启用了超线程,则逻辑内核最多只能使用其四个通用内核中的两个计数器。 MEM_UOPS_RETIRED.* 事件是受该错误影响的事件之一。您可以做的一件事是禁用超线程。

    了解使用这些事件可以衡量什么样的“缓存命中率”非常重要。您可能不想测量没有意义的东西。一个可能有意义的比率是L1-dcache-load-misses / (L1-dcache-loads + L1-dcache-stores),它表示出于任何原因的 L1D 替换的数量(缓存中填充的行导致其他人被驱逐)除以退休的数量加载和存储微指令。并非所有未命中都会导致替换,并且所有未命中的很大一部分可能会击中 LFB,这也不会导致替换。此外,并非所有替换都是由最终退出的微指令访问引起的。

    【讨论】:

      【解决方案3】:

      您可能对gprof(1) 感兴趣。它不会测量缓存命中率(这没有任何意义,因为在启用优化的情况下调用 GCC 后,一些对 foo 的调用可能会被内联)。

      您可以在代码中使用libbacktrace。另见time(7)signal(7)

      您可以使用gcc -Wall -Wextra -O2 -g -pg 编译您的代码,然后在其中使用libbacktrace(就像GCCRefPerSys 正在做的那样),然后在其中使用gprof(1)gdb(1)

      通过努力(请阅读Advanced Linux Programming,然后阅读syscalls(2)signal-safety(7)),您可以将setitimer(2)sigaction(2) 和/或profil(3) 一起使用。

      考虑生成一些 C 代码(例如,在您自己的 C 代码生成器中使用GPP 和/或GNU bison)并查看this answer。 J.Pitrat 的书 Artificial Beings: the Conscience of a Conscious Machine (ISBN-13: 978-1848211018) 可能会鼓舞人心。您可能希望生成一些 C 代码以进行额外检测。

      您可能会在运行时在插件中生成一些代码(例如使用libgccjitGNU lightning...),然后生成dlopen(3)dlsym(3) 它。阅读有关partial evaluation 的更多信息并查看我的manydl.c 示例,更认真地阅读OcamlSBCL 的源代码。

      您可以编写 GCC plugin 以自动生成一些测量值,这比 GCC 的 -pg 选项更聪明。您的 GCC 插件将(在 GIMPLE 级别)将大多数函数调用转换为更复杂的东西,从而进行一些基准测试(这就是 -pg 在 GCC 中的工作方式,您可以研究 GCC 的源代码)。尝试将您的foo.c 编译为gcc -Wall -Wextra -O2 -pg -S -fverbose-asm foo.c 并查看生成的foo.s,可能会添加更多optimizationsstatic analysisinstrumentation 选项。

      你可能对ACM SIGPLAN最近的论文感兴趣。

      最后,对没有优化编译的 C 程序进行基准测试是没有意义的。考虑改为使用至少gcc -flto -O2 -Wall 编译和链接您的程序

      在您的foo 中,您可以巧妙地使用clock_gettime(2) 来测量CPU 时间。

      如果性能非常重要,并且允许您花费数周的时间来改进它,您可能会考虑使用OpenCL(或者可能是CUDA)在强大的 GPGPU 上计算您的内核。当然,您需要专用硬件。否则,请考虑使用OpenMPOpenACC(或者MPI)。一些最近的 GCC 编译器(至少 2020 年 10 月的 GCC 10)可以支持这些。当然,阅读Invoking GCC上的文档

      【讨论】:

      • 感谢您的回答。但我认为这与问题无关。
      • 我们不明白您的问题。考虑用一些用 C 编码的minimal reproducible example 来改进它。你应该使用profil(3)
      猜你喜欢
      • 1970-01-01
      • 2011-10-16
      • 1970-01-01
      • 2011-10-31
      • 2015-08-27
      • 2017-01-15
      • 2016-02-10
      • 1970-01-01
      • 2020-08-08
      相关资源
      最近更新 更多