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这套简单直接的文本工作流依然很能打。老将出马,一个顶俩。

Leave a Reply

Your email address will not be published. Required fields are marked *