17、性能分析工具(上):gprof的使用,如何解读调用图与热点函数。

性能调优这件事,说白了就是“先测量,再动手”。

我见过不少工程师,上来就凭直觉改代码,觉得某个循环慢,就加个优化选项,结果折腾半天,性能没提升,反而把逻辑搞乱了。其实,真正高效的优化,第一步永远是——找到瓶颈在哪。

今天我们就聊聊 gprof,这个经典的 GNU 性能分析工具。虽然它不像 perf 那么现代,但胜在简单、直观,特别适合嵌入式环境下的快速分析。我个人习惯在项目早期先用 gprof 跑一遍,心里有个底,再决定要不要上更复杂的工具。

17.1 gprof 的工作原理

gprof 的核心机制,说白了就是“采样 + 插桩”。

编译时加上 -pg 选项,编译器会在每个函数的入口和出口插入监控代码。程序运行时,这些代码会记录函数调用关系、调用次数、以及每个函数的执行时间。

程序退出时,这些数据会写入 gmon.out 文件。然后你用 gprof 命令解析这个文件,就能得到调用图和热点函数列表。

关键点: gprof 是基于“统计采样”的,不是精确计时。它适合分析长时间运行的程序,对于短时间(比如几毫秒)的调用,误差可能较大。

我曾经在一个嵌入式项目中,用 gprof 分析一个通信协议栈。程序跑了大概 10 分钟,gprof 报告显示某个校验函数占了 40% 的 CPU 时间。我一开始还不信,觉得那个函数很简单。后来仔细一看,原来它在循环里被调用了上百万次,每次只做一点点计算,但积少成多,就成了热点。

17.2 如何使用 gprof

使用步骤很简单,就三步:

  1. 编译时加 -pg 选项
  2. 正常运行程序(程序退出后会自动生成 gmon.out
  3. 运行 gprof 命令生成报告

来看一个具体的例子。假设我们有这样一个简单的程序:

// main.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_a();
    func_b();
    return 0;
}

编译和运行:

gcc -pg -o test main.c
./test
gprof test gmon.out > report.txt

打开 report.txt,你会看到两部分内容:平坦分析(Flat Profile)调用图(Call Graph)

17.3 解读平坦分析(Flat Profile)

平坦分析,说白了就是一张“谁最耗时”的排行榜。它告诉你每个函数占用了多少 CPU 时间,以及被调用了多少次。

典型的输出格式如下:

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 60.0      0.06     0.06        2    30.00    30.00  func_a
 40.0      0.10     0.04        1    40.00    70.00  func_b
  0.0      0.10     0.00        1     0.00    70.00  main

我来解释一下各列的含义:

列名 含义
% time 该函数占用的 CPU 时间百分比
cumulative seconds 累计时间(从程序开始到该函数结束)
self seconds 函数自身执行的时间(不包括子函数)
calls 函数被调用的次数
self ms/call 每次调用自身消耗的时间(毫秒)
total ms/call 每次调用总消耗时间(包括子函数)

我的习惯: 先看 % time 列,找到占比最高的函数。如果某个函数占了 30% 以上,那它大概率就是优化目标。然后看 self ms/call,判断这个函数本身是否耗时,还是它的子函数在拖后腿。

17.4 解读调用图(Call Graph)

调用图展示的是函数之间的调用关系,以及每条调用路径上的时间开销。它比平坦分析更详细,能帮你理解“时间到底花在了哪条路径上”。

典型的调用图输出:

Call graph (explanation follows)


granularity: each sample hit covers 2 byte(s) for 10.00% of 0.10 seconds

index % time    self  children    called     name
                0.06    0.00       2/2         main [2]
[1]     60.0    0.06    0.00       2         func_a [1]
-----------------------------------------------
                0.04    0.06       1/1         main [2]
[2]    100.0    0.04    0.06       1         func_b [2]
                0.06    0.00       1/1         func_a [1]
-----------------------------------------------
                0.00    0.10       1/1         <spontaneous>
[3]    100.0    0.00    0.10       1         main [3]
                0.04    0.06       1/1         func_b [2]
                0.06    0.00       1/1         func_a [1]

嗯,这里要注意,调用图的格式看起来有点乱,但核心信息就几点:

  • 方括号里的数字(如 [1])是函数索引,方便交叉引用。
  • self 列:函数自身消耗的时间。
  • children 列:该函数调用的子函数消耗的时间。
  • called 列:调用次数,格式是 被调用次数/总调用次数
  • 缩进关系:缩进的函数是被上一级函数调用的。

举个例子,看 [2] 这一块:

  • func_bmain 调用了 1 次。
  • func_b 自身消耗了 0.04 秒。
  • func_b 调用了 func_a,子函数消耗了 0.06 秒。
  • 所以 func_b 的总时间是 0.04 + 0.06 = 0.10 秒。

避坑指南: 我曾经在解读调用图时,看到某个函数 self 时间很小,但 children 时间很大,就以为它不重要。后来才发现,它调用了大量子函数,这些子函数才是真正的热点。所以,一定要结合 selfchildren 一起看,别只看表面。

17.5 如何找到真正的热点函数

热点函数,就是那些占用 CPU 时间最多的函数。但要注意,热点不一定在“最外层”的函数里。有时候,一个看似简单的函数,因为被调用了成千上万次,反而成了瓶颈。

我的经验是,按以下步骤来定位:

  1. 先看平坦分析,找到 % time 最高的前 3-5 个函数。
  2. 再看调用图,确认这些函数是被谁调用的,调用路径是否合理。
  3. 检查 self ms/call,如果某个函数每次调用耗时很长(比如超过 1 毫秒),那它本身可能就是优化目标。
  4. 检查调用次数,如果某个函数被调用了上百万次,即使每次只花 1 微秒,累计起来也很可观。

核心原则: 优化“调用次数多”的函数,比优化“单次耗时长”的函数,往往收益更大。因为前者是乘法效应,后者是加法效应。

17.6 知识体系图

下面这张图,帮你梳理 gprof 的核心流程和解读思路:

gprof 性能分析流程 1. 编译加 -pg gcc -pg -o test test.c 2. 运行程序 ./test 3. 生成报告 gprof test gmon.out 输出 gmon.out 平坦分析 (Flat Profile) 谁最耗时?调用次数? 调用图 (Call Graph) 调用路径?时间分布? 解读要点 • 平坦分析:关注 % time 列,找到占比最高的函数 • 调用图:关注 self 和 children 列,区分自身耗时和子函数耗时 • 热点定位:结合调用次数和单次耗时,优先优化“调用次数多”的函数 注意:gprof 适合长时间运行的程序,短时间程序误差较大

17.7 实战中的注意事项

最后,分享几个我在实际项目中踩过的坑:

  • 别忘了加 -pg 编译所有文件。 如果只给主文件加,其他文件没有插桩,gprof 会漏掉很多信息。
  • 程序正常退出才能生成 gmon.out 如果程序被 kill -9 或者崩溃了,数据就没了。我建议在程序退出前加个 atexit() 回调,确保数据写入。
  • 多线程程序慎用 gprof。 gprof 对多线程的支持很弱,它只能分析主线程。如果你用多线程,建议换 perf 或 Valgrind。
  • 优化后记得重新编译。 这个听起来像废话,但我真见过有人改完代码忘了重新编译,拿着旧报告分析半天。

小技巧: 如果你觉得 gprof 的输出太乱,可以用 gprof -b 去掉解释性文字,只保留数据部分。或者用 gprof -l 显示行级别的信息,定位到具体代码行。

好了,gprof 的基本用法和解读方法就聊到这里。它虽然老,但胜在简单可靠。下次你遇到性能问题,不妨先跑一遍 gprof,看看热点在哪,再动手优化。相信我,这比瞎猜要高效得多。

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