性能调试:perf与gprof热点分析

性能调试这事儿,说白了就是找“慢在哪”。

我早年做嵌入式项目时,经常遇到这种情况:程序跑起来了,功能也对,但就是卡。用户按个键,要等两秒才有反应。老板站在背后问:“能不能优化一下?” 你只能硬着头皮说“能”。但怎么优化?你得先知道瓶颈在哪。

今天聊的两个工具——perfgprof,就是干这个的。它们能帮你找到代码里的“热点”,也就是最耗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 的定位:

性能调试工具选择 热点分析 gprof(插桩) perf(采样) 需重新编译 函数级统计 无需重新编译 指令级统计 适用:小型项目、单线程 适用:大型项目、多线程、内核 选择依据:项目规模、是否需要内核分析、是否可重新编译

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 的基本用法就这些。你想想看,下次遇到程序卡顿,是不是知道该从哪里下手了?


公众号:蓝海资料掘金营,微信deep3321