性能调试:perf与gprof热点分析
性能调试这事儿,说白了就是找“慢在哪”。
我早年做嵌入式项目时,经常遇到这种情况:程序跑起来了,功能也对,但就是卡。用户按个键,要等两秒才有反应。老板站在背后问:“能不能优化一下?” 你只能硬着头皮说“能”。但怎么优化?你得先知道瓶颈在哪。
今天聊的两个工具——perf 和 gprof,就是干这个的。它们能帮你找到代码里的“热点”,也就是最耗CPU时间的地方。
1. 先搞清楚:采样 vs 插桩
这两个工具的原理完全不同。我简单解释一下:
- gprof 是“插桩”式的。编译时加个
-pg选项,编译器会在每个函数入口插入统计代码。程序跑完后,生成一个gmon.out文件,里面记录了每个函数被调用了多少次、花了多少时间。 - perf 是“采样”式的。它利用CPU的性能计数器,每隔一段时间(比如1ms)打断一下程序,看看当前在跑哪段代码。统计大量样本后,就能知道哪些函数占用的CPU时间最多。
嗯,这里要注意:gprof 需要重新编译代码,perf 不需要。perf 可以直接分析一个已经在跑的程序,甚至分析整个系统。
核心区别一句话: gprof 告诉你“每个函数花了多少时间”,perf 告诉你“CPU 在哪些指令上花了最多时间”。
2. gprof 实战:从编译到分析
我个人习惯在小型项目或单机程序上用 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++) {
// 模拟计算
}
}
int main() {
func_a();
func_b();
return 0;
}
编译时加上 -pg:
gcc -pg -o test test.c
./test
gprof test gmon.out > report.txt
打开 report.txt,你会看到类似这样的输出:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
66.67 0.02 0.02 1 20.00 20.00 func_a
33.33 0.03 0.01 1 10.00 10.00 func_b
0.00 0.03 0.00 1 0.00 0.00 main
你看,func_a 占了 66.67% 的时间。这就是热点。
小技巧: 如果 gmon.out 文件很大,可以用 gprof -l 看行级别的热点,精确到每一行代码。
3. perf 实战:系统级热点分析
perf 更强大。我在项目中遇到过一个问题:一个多线程程序,CPU 占用率 100%,但 gprof 分析不出来,因为 gprof 只统计主线程。perf 可以看所有线程。
基本用法:
# 采样 10 秒
perf record -g ./test
# 生成报告
perf report
-g 选项会记录调用链,这样你不仅知道哪个函数热,还知道是谁调了它。
输出界面大概是这样的(文本模式):
Samples: 1K of event 'cycles'
Event count (approx.): 850000000
Overhead Command Shared Object Symbol
45.23% test test [.] func_a
30.15% test test [.] func_b
12.50% test libc-2.31.so [.] __libc_write
5.00% test [kernel] [k] do_syscall_64
你看,连系统调用都给你标出来了。这就是 perf 的厉害之处。
注意: perf 需要 root 权限才能访问硬件性能计数器。如果没权限,可以用 perf stat 做统计,但看不到调用链。
4. 热点分析的常见误区
我曾经踩过一个坑:用 gprof 分析一个 I/O 密集型的程序,结果发现大部分时间花在 read() 函数上。我以为是 read() 太慢,拼命优化它。后来才发现,真正的问题是程序在等磁盘数据,CPU 根本没干活。gprof 统计的是 CPU 时间,不是墙钟时间。
所以,你要搞清楚:
- CPU 密集型:用 perf 或 gprof 都行。
- I/O 密集型:用
perf stat看上下文切换次数,或者用strace看系统调用耗时。 - 多线程竞争:用
perf record -e sched:sched_switch看调度情况。
5. 知识体系图
下面这张图帮你理清 perf 和 gprof 的定位:
6. 实战建议:什么时候用哪个?
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| 单线程、纯计算 | gprof | 简单,输出直观 |
| 多线程、高并发 | perf | 能看所有线程,支持调用链 |
| I/O 密集型 | perf stat + strace | perf 看 CPU 利用率,strace 看系统调用 |
| 内核模块 | perf | gprof 不支持内核代码 |
| 嵌入式交叉编译 | gprof | perf 需要内核支持,嵌入式环境不一定有 |
我的习惯: 先用 perf stat 看一眼整体情况,比如 CPU 利用率、上下文切换次数。如果发现 CPU 利用率高,再用 perf record 抓热点。如果 CPU 利用率低,说明程序在等 I/O,那就换 strace 或 iostat。
7. 避坑指南
我曾经在调试一个实时系统时,用 gprof 分析后发现一个函数占了 80% 的时间。我花了两天优化它,结果性能没提升。后来才发现,gprof 的统计方式会引入额外开销,尤其是频繁调用的短函数。gprof 的“自时间”可能不准。
所以,我的建议是:
- 不要只信一个工具。用 perf 验证一下 gprof 的结果。
- 注意优化级别。用
-O2编译时,编译器可能内联函数,导致 gprof 统计不到。perf 不受影响。 - 采样时间要够长。perf 默认采样 1 秒,如果程序跑得很快,可能采不到足够样本。加长到 10 秒以上。
重要提醒: 性能调试不是一次性的。改完代码后,一定要重新跑一遍 perf 或 gprof,确认热点确实被优化掉了。我见过有人改完代码,性能反而更差了,因为“优化”引入了新的瓶颈。
好了,perf 和 gprof 的基本用法就这些。你想想看,下次遇到程序卡顿,是不是知道该从哪里下手了?