4. 性能分析入门:Perf、gprof 的基本使用与火焰图生成

性能分析这件事,说白了就是回答三个问题:哪里慢?为什么慢?怎么改? 我见过不少工程师,代码写得飞快,一上板子就卡成幻灯片,然后开始瞎猜——「是不是中断太多了?」「是不是DMA没配好?」。猜来猜去,不如直接拿工具说话。

这一章,我们就来聊聊嵌入式Linux下最常用的两个性能分析工具:Perf 和 gprof,以及怎么把它们的输出变成直观的火焰图。嗯,这些都是我项目里反复用过的东西,踩过的坑也不少,今天一并倒给你。

4.1 为什么需要性能分析工具?

你想想看,一个嵌入式系统跑起来,CPU在忙什么?是真正在算业务逻辑,还是在等内存、等锁、等I/O?没有工具,你只能靠printf打时间戳——这招我早年也用过,但一旦函数调用深了、线程多了,根本打不过来。

性能分析工具能帮你做到三件事:

  • 定位热点函数:哪个函数吃掉了最多的CPU时间。
  • 分析调用关系:谁调了谁,调用频次如何。
  • 发现性能瓶颈:是算法问题,还是系统层面的调度/缓存问题。

核心观点:没有数据支撑的优化,都是玄学。用工具说话,别靠直觉。

4.2 gprof:GNU的经典性能分析工具

gprof 是 GNU 工具链自带的性能分析器。它的原理很简单:编译时在函数入口和出口插入采样代码,运行时收集调用次数和时间,最后生成一份报告。

4.2.1 使用步骤

  1. 编译时加 -pg 选项
  2. 正常运行程序(正常退出,会生成 gmon.out 文件)
  3. 运行 gprof 分析

来看一个具体的例子。假设我们有这么一段代码:

// test.c
#include <stdio.h>

void func_a() {
    int i;
    for (i = 0; i < 1000000; i++);
}

void func_b() {
    int i;
    for (i = 0; i < 500000; i++);
    func_a();
}

int main() {
    func_b();
    func_a();
    return 0;
}

编译和运行:

$ gcc -pg -o test test.c
$ ./test
$ gprof test gmon.out

输出结果中,最重要的部分是 Flat Profile 和 Call Graph:

函数名 调用次数 自身时间(秒) 累计时间(秒)
func_a 2 0.02 0.02
func_b 1 0.01 0.03
main 1 0.00 0.03

注意:gprof 只能分析用户态函数,内核态调用它看不到。另外,它依赖程序正常退出(调用 exit 或从 main return)来写 gmon.out,如果程序被 kill -9 杀掉,数据就丢了。我曾经在调试一个后台守护进程时就吃过这个亏——程序跑着跑着被看门狗复位,gmon.out 永远出不来。

4.2.2 gprof 的局限性

  • 需要重新编译,无法分析线上二进制。
  • 对多线程程序支持不好(只统计主线程)。
  • 采样精度受限于编译器插入的 instrumentation 代码本身的开销。

所以,gprof 更适合做单线程、开发阶段的快速分析。到了线上、多线程、内核态混合的场景,就得请出 Perf 了。

4.3 Perf:Linux 内核级的性能分析利器

Perf 是 Linux 内核自带的性能分析工具。它利用硬件 PMU(Performance Monitoring Unit)和内核 tracepoint 来采样,不需要重新编译程序,开销极低。

4.3.1 基本用法

我个人最常用的几个 Perf 命令:

命令 作用
perf stat 统计程序运行时的硬件事件(周期、指令数、缓存miss等)
perf record 采样并记录性能数据到文件
perf report 展示采样结果
perf top 实时显示当前系统热点函数

先看一个最简单的例子,统计 ls 命令的硬件事件:

$ perf stat ls

输出类似:

 Performance counter stats for 'ls':

          1.234567      task-clock (msec)         #    0.987 CPUs utilized
                12      context-switches          #    0.010 M/sec
                 0      cpu-migrations            #    0.000 K/sec
               105      page-faults               #    0.085 M/sec
         3,456,789      cycles                    #    2.800 GHz
         2,100,000      instructions              #    0.61  insn per cycle
           450,000      cache-references          #    0.364 M/sec
            12,000      cache-misses              #    2.667 % of all cache refs

       0.001250000 seconds time elapsed

看到 insn per cycle 了吗?这个值如果远小于1,说明 CPU 经常在等内存或等 I/O,而不是在真正算东西。我在优化一个网络协议栈时,发现这个值只有 0.3,后来定位到是频繁的 cache miss 导致的——改了一下数据结构的内存布局,直接提到了 0.8。

4.3.2 采样热点函数

要找到程序里最耗时的函数,用 perf record 加 perf report:

$ perf record -g ./test
$ perf report

-g 选项会记录调用栈,这样你不仅知道哪个函数热,还知道它是被谁调进来的。输出是一个交互式的 TUI 界面,按箭头键可以展开调用链。

小技巧:在嵌入式板子上,如果屏幕太小或者没有显示终端,可以用 perf script 把采样数据导出成文本,然后拿到 PC 上分析。

4.4 火焰图:让性能数据一目了然

Perf report 的文本界面虽然信息全,但不够直观。尤其是调用栈很深的时候,一层一层展开,看得人头晕。这时候,火焰图就派上用场了。

火焰图是 Brendan Gregg 发明的可视化方式。它的特点:

  • X 轴:采样数量(宽度越宽,说明该路径耗时越多)。
  • Y 轴:调用栈深度(越往上,调用越深)。
  • 颜色:通常随机分配,没有特殊含义(别被颜色误导)。

生成火焰图的流程:

  1. 用 perf record -g 采集数据。
  2. 用 perf script 导出为文本。
  3. 用 Brendan Gregg 的 FlameGraph 工具集生成 SVG。

具体命令:

$ perf record -g -F 99 ./test
$ perf script > out.perf
$ ./stackcollapse-perf.pl out.perf > out.folded
$ ./flamegraph.pl out.folded > flame.svg

这里 -F 99 表示每秒采样 99 次。为什么是 99?不是 100?因为 99 是质数,可以避免与程序的周期性行为(比如定时器中断)产生共振,采样更随机。嗯,这个小细节,我也是看了 Brendan Gregg 的博客才学到的。

怎么看火焰图? 找最宽的「平顶山」——那就是最耗时的调用路径。如果山顶很宽,说明这个函数本身很耗时;如果山腰很宽,说明它调用的子函数耗时。

4.5 知识体系:性能分析工具选型

为了让你一眼看清不同工具的适用场景,我画了一张图:

性能分析工具选型框架 性能分析需求 开发阶段 / 单线程 线上 / 多线程 / 内核 可视化 / 汇报 gprof perf record / perf top FlameGraph 工具集 Flat Profile + Call Graph 硬件事件 + 调用栈采样 SVG 火焰图 选型建议 • 开发阶段快速验证 → gprof(简单,但需重新编译) • 线上/多线程/内核态 → perf(无侵入,功能强) • 汇报/团队沟通 → 火焰图(直观,一眼看出热点)

4.6 实战:用 Perf + 火焰图优化一个真实问题

我记得有一次,一个同事抱怨说他的数据采集程序在 ARM 板子上跑得很慢,每秒只能处理 1000 个包。我们先用 perf top 看了一眼:

$ perf top

结果发现,排名第一的热点函数是 memcpy,占了 40% 的 CPU 时间。再往下看,调用 memcpy 的是 ring_buffer_write。原来他每次收到一个包,都先拷贝到一个环形缓冲区里——但缓冲区开得太小,频繁触发 memcpy 搬数据。

解决方案很简单:增大环形缓冲区,减少 memcpy 次数。改完后,处理能力直接翻了三倍。

经验之谈:很多时候,性能瓶颈不在算法复杂度,而在数据拷贝、锁竞争、缓存 miss 这些「低级」问题上。先用 perf 扫一遍,别急着优化代码逻辑。

4.7 避坑指南

  • perf 需要 root 权限:在嵌入式板子上,记得用 sudo 或者把 kernel.perf_event_paranoid 设为 -1。
  • 采样频率别太高:-F 99 通常够用,设成 1000 以上反而可能影响程序行为,产生「观察者效应」。
  • gprof 的计时不准:它用的是 clock(),精度只有 10ms 级别。短函数的时间可能被低估或高估。
  • 火焰图不是银弹:它只能告诉你「哪里占 CPU 多」,但回答不了「为什么占 CPU 多」。比如一个函数因为频繁 cache miss 而慢,火焰图上只会显示它很宽,但原因需要结合 perf stat 的 cache-misses 指标来分析。

我曾经踩过的坑:有一次我在一个多线程程序上用 perf record -g,结果生成的火焰图全是乱序的调用栈。后来才发现,perf 默认只采样当前线程的调用栈,而我的程序里线程 A 调了线程 B 的函数,但采样点落在了线程 B 上,调用栈就断了。解决办法是加上 --call-graph dwarf 或者用 --call-graph lbr(如果硬件支持)。

4.8 小结

这一章我们聊了 gprof 和 Perf 的基本用法,以及怎么把 Perf 的采样数据变成火焰图。工具本身不复杂,难的是养成用数据说话的习惯。下次你觉得程序慢的时候,别急着改代码,先跑一遍 perf,看看数据怎么说。