4. 性能分析入门:Perf、gprof 的基本使用与火焰图生成
性能分析这件事,说白了就是回答三个问题:哪里慢?为什么慢?怎么改? 我见过不少工程师,代码写得飞快,一上板子就卡成幻灯片,然后开始瞎猜——「是不是中断太多了?」「是不是DMA没配好?」。猜来猜去,不如直接拿工具说话。
这一章,我们就来聊聊嵌入式Linux下最常用的两个性能分析工具:Perf 和 gprof,以及怎么把它们的输出变成直观的火焰图。嗯,这些都是我项目里反复用过的东西,踩过的坑也不少,今天一并倒给你。
4.1 为什么需要性能分析工具?
你想想看,一个嵌入式系统跑起来,CPU在忙什么?是真正在算业务逻辑,还是在等内存、等锁、等I/O?没有工具,你只能靠printf打时间戳——这招我早年也用过,但一旦函数调用深了、线程多了,根本打不过来。
性能分析工具能帮你做到三件事:
- 定位热点函数:哪个函数吃掉了最多的CPU时间。
- 分析调用关系:谁调了谁,调用频次如何。
- 发现性能瓶颈:是算法问题,还是系统层面的调度/缓存问题。
核心观点:没有数据支撑的优化,都是玄学。用工具说话,别靠直觉。
4.2 gprof:GNU的经典性能分析工具
gprof 是 GNU 工具链自带的性能分析器。它的原理很简单:编译时在函数入口和出口插入采样代码,运行时收集调用次数和时间,最后生成一份报告。
4.2.1 使用步骤
- 编译时加 -pg 选项
- 正常运行程序(正常退出,会生成 gmon.out 文件)
- 运行 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 轴:调用栈深度(越往上,调用越深)。
- 颜色:通常随机分配,没有特殊含义(别被颜色误导)。
生成火焰图的流程:
- 用
perf record -g采集数据。 - 用
perf script导出为文本。 - 用 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 知识体系:性能分析工具选型
为了让你一眼看清不同工具的适用场景,我画了一张图:
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,看看数据怎么说。