OProfile尝。。鲜??
最近使用了一个叫OProfile的用于程序性能测试的开源项目,在这里记录一下使用感受和心得。
什么是OProfile?
OProfile诞生于2002年,已经有着18年的历史了。该项目主要用于Linux系统的性能剖析,采用记录CPU事件(events)的方式,最终给使用者提供程序或系统的性能统计数据。
下载安装
OProfile可以从这个网站下载,小编用的是写这篇时最新的1.4.0版本。下载完成后用如下简单命令编译安装:
./configure
make
make install
编译前记得先装好libpopt和binutils的开发包(Debian系对应的是libpopt-dev和binutils-dev)。如果不想折腾源码,Debian系的系统也可以直接sudo apt install oprofile,只是版本会旧一些。
因为主要用于监测Linux系统性能,所以当然只能在Linux上编译啦。
小编测试环境使用的是当下流行的树莓派3B+,编译器使用的是GCC 10。
使用
老版本的OProfile用的是opcontrol加内核模块那一套,1.0之后已经彻底废弃了。现在的主力是三个命令:operf(采样)、opreport(看报告)和opannotate(标注到源码)。它们底层用的是内核的perf_events接口,不需要额外加载内核模块,普通用户就能剖析自己的程序。
准备一个“有问题”的程序
为了试用,我们先写一个自带热点的小程序——经典的朴素矩阵乘法(matmul.c):
#include <stdio.h>
#define N 512
static double a[N][N], b[N][N], c[N][N];
static void mat_init(void)
{
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) {
a[i][j] = i + j;
b[i][j] = i - j;
}
}
static void mat_mul(void)
{
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
for (int k = 0; k < N; k++)
c[i][j] += a[i][k] * b[k][j];
}
int main(void)
{
mat_init();
mat_mul();
printf("c[0][0] = %f\n", c[0][0]);
return 0;
}
编译时记得带上-g,这样后面才能把采样对应回源码;优化照常开-O2,毕竟我们要剖析的是“真实”的程序:
gcc -O2 -g -o matmul matmul.c
用operf采样
$ operf ./matmul
operf: Profiler started
c[0][0] = 44608256.000000
Profiling done.
就这么简单,不需要任何参数。采样数据会写进当前目录下的oprofile_data/里,opreport等工具默认也从这里读取。默认采样的事件是CPU_CYCLES,也就是“这段时间CPU的周期都花在哪了”。
用opreport看报告
先看整体情况(以下输出为摘录,具体数字在不同环境下会不一样,下同):
$ opreport
CPU: ARM Cortex-A53, speed 1400 MHz (estimated)
Counted CPU_CYCLES events (Cycle) with a unit mask of 0x00 (No unit mask) count 100000
CPU_CYCLES:100000|
samples| %|
------------------
41273 98.6120 matmul
412 0.9844 libc-2.28.so
169 0.4037 ld-2.28.so
再用-l细化到函数级别:
$ opreport -l ./matmul
CPU: ARM Cortex-A53, speed 1400 MHz (estimated)
Counted CPU_CYCLES events (Cycle) with a unit mask of 0x00 (No unit mask) count 100000
samples % symbol name
41109 99.6027 mat_mul
164 0.3973 mat_init
99.6%的周期都花在mat_mul里——热点一目了然。
用opannotate对应到源码
$ opannotate --source ./matmul
...
:static void mat_mul(void)
:{
3 0.0073 : for (int i = 0; i < N; i++)
214 0.5206 : for (int j = 0; j < N; j++)
2163 5.2616 : for (int k = 0; k < N; k++)
38729 94.2105 : c[i][j] += a[i][k] * b[k][j];
:}
可以看到绝大部分采样都落在最内层那一行乘加上。这一步能工作的前提,就是前面编译时带上的那个-g。
换个事件:看看cache
CPU_CYCLES能告诉你“慢在哪”,但不解释“为什么慢”。用ophelp可以列出当前CPU支持的所有事件,树莓派3B+的Cortex-A53上就有L1D_CACHE_REFILL(L1数据缓存未命中)这类事件。我们用它再采一次:
$ operf -e L1D_CACHE_REFILL:10000 ./matmul
$ opreport -l ./matmul
朴素矩阵乘法对b[k][j]的访问在内存里是跳着走的,缓存命中率很差。把三重循环从i-j-k换成i-k-j的顺序再各跑一遍对比,小编环境里L1D_CACHE_REFILL的采样数差了接近一个数量级,运行时间也缩短了一大半。先用周期定位热点,再换事件解释热点——这基本就是性能剖析的标准套路了。
系统级剖析
除了盯着单个程序,operf也可以剖析整个系统(需要root),Ctrl-C结束采样:
$ sudo operf --system-wide
(跑一会儿你想观察的负载,然后Ctrl-C)
$ sudo opreport
适合回答“这台机器的CPU到底被谁吃了”这类问题。
使用感受和心得
- 上手很快:operf → opreport → opannotate三板斧就能覆盖大部分场景,输出全是纯文本,在只有串口的嵌入式板子上用起来也很舒服。
- 项目确实老了:文档和社区都比较冷清,网上搜到的资料一多半还在讲已经废弃的opcontrol,照着抄是跑不起来的——认准operf就好。
- 生态不如perf:如今operf和perf底层用的是同一套perf_events机制,但perf有火焰图等一整套生态。如果你用的是x86服务器,直接上perf可能更省心。
- ARM板子上要看内核脸色:能不能采样取决于内核有没有启用PMU(设备树里要有arm-pmu节点)。如果operf报错说不支持你的处理器,先去查内核配置,而不是怀疑人生。
总的来说,“鲜”是谈不上了——这个2002年出生的项目今年都18岁了。但在树莓派这类嵌入式Linux环境里,想快速回答“哪个函数最热”的问题,OProfile这套简单直接的文本工作流依然很能打。老将出马,一个顶俩。