分析C程序函数的缓存命中率



我想获得在Linux机器上运行的C/C++程序(foo)的特定函数的缓存命中率。我使用的是gcc,没有编译器优化。使用perf,我可以使用以下命令获得整个程序的命中率。

perf-stat-e一级缓存加载,一级缓存负载未命中,一级内存存储,一级存储未命中/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 Memoryn");
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

您也可以使用Likwid及其Marker-API。它使对代码的某些区域进行检测变得非常容易。您可以在haswell体系结构上为一级缓存未命中率使用预定义的性能组ICACHE,也可以为一级命中率定义自己的性能组。

#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 <your application>

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

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

您可以在代码中使用libbacktrace。另请参见时间(7)和信号(7)。

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

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

还可以考虑生成一些C代码(例如,在您自己的C代码生成器中使用GPP和/或GNU bison),然后看到这个答案。J.Pitrat的书《人造人:有意识机器的良心》(ISBN-13:978-1848211018)可能是鼓舞人心的。您可能需要生成一些C代码来进行额外的插入。

您可能会在运行时在插件中生成一些代码(例如使用libgccjit或GNU闪电…),然后使用dlopen(3)和dlsym(3)。阅读更多关于部分评估的信息,并查看我的manydl.c示例,更重要的是Ocaml或SBCL的源代码。

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

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

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

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

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

首先,请注意,您的处理器不支持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-5690X中确实存在(它记录在其他Haswell处理器和其他一些微体系结构的处理器的规范更新文件中)。不同版本的perf对该错误的处理方式不同。从内核版本4.1-rc7开始,如果在逻辑内核上启用了受该错误影响的事件之一,并且在启动时启用了超线程,则逻辑内核最多只能使用四个通用计数器中的两个。MEM_UOPS_RETIRED.*事件是受该错误影响的事件之一。您可以做的一件事是禁用超线程。

了解什么样的";高速缓存命中率";可以用这些事件来衡量。你可能不想测量一些没有意义的东西。一个可能有意义的比率是L1-dcache-load-misses/(L1-dcache-loads+L1-dcache-stores),它表示出于任何原因的L1D替换(填充在缓存中导致其他缓存被逐出的行)的数量除以失效的加载和存储uop的数量。并不是所有的失误都会导致替换,所有失误中有很大一部分可能发生在LFB中,而LFB也不会导致替换。此外,并不是所有的替换都是由最终退出的uop的访问引起的。

相关内容

  • 没有找到相关文章

最新更新