2. Profiling工具入门:gprof的安装与使用,生成调用图,分析热点函数

性能优化这件事,说白了就是“先测量,再动手”。你想想看,如果连代码哪里慢都不知道,上来就瞎改一通,那跟闭着眼修车有什么区别?我见过太多人,一上来就盯着某个函数猛优化,结果发现瓶颈根本不在那儿。嗯,这就是为什么我们今天要聊gprof——一个经典但依然好用的性能剖析工具。

2.1 gprof是什么?它能帮你解决什么问题?

gprof是GNU工具链里自带的性能分析工具。它的工作方式很简单:在程序编译时插入监控代码,运行时收集每个函数的调用次数和执行时间,最后生成一份报告。这份报告能告诉你两件事——哪个函数被调得最多,哪个函数跑得最慢。

我个人习惯把gprof当作“第一轮筛查工具”。为什么?因为它不需要改代码,编译时加个选项就行。对于大型项目,你不可能一开始就上perf或者SystemTap那种重型武器。先用gprof扫一遍,找到可疑的热点函数,再针对性地深入分析,这才是高效的做法。

核心价值:gprof能帮你快速回答两个问题——"我的程序时间花在哪了?"和"哪个函数被调用了太多次?"

2.2 安装gprof:其实你大概率已经有了

gprof是GNU binutils的一部分。如果你用的是Linux,基本都预装了。不信你打开终端敲一下:

gprof --version

如果看到版本号,恭喜你,省事了。如果没有,那就装一下:

# Ubuntu/Debian
sudo apt-get install binutils

# CentOS/RHEL
sudo yum install binutils

# macOS (通过Homebrew)
brew install gprof

我记得有一次在客户的嵌入式环境里,他们用的是一套裁剪过的Linux,连gprof都没有。我当时只能手动编译了一个静态链接的binutils放进去。嗯,那种环境真是让人头疼。不过大多数情况下,你不需要操心这个。

2.3 使用gprof:三步走

使用gprof其实就三步:编译时加选项、运行程序、分析报告。我们一步步来。

2.3.1 编译时加 -pg 选项

这是最关键的一步。不加这个选项,gprof就收集不到数据。

gcc -pg -o myprogram myprogram.c -lm

注意:-pg 必须加在编译和链接两个阶段。如果你分开编译和链接,两个步骤都要加:

gcc -pg -c myprogram.c -o myprogram.o
gcc -pg myprogram.o -o myprogram -lm

我曾经踩过的坑:有一次我忘了在链接阶段加 -pg,结果编译通过了,运行时也生成了 gmon.out,但 gprof 报告全是空的。折腾了半天才发现是链接时漏了选项。所以记住——编译和链接都要加 -pg。

2.3.2 运行程序,生成 gmon.out

编译好之后,正常运行程序:

./myprogram

程序退出后,当前目录下会生成一个 gmon.out 文件。这个文件就是gprof的原始数据。注意:如果程序异常退出(比如段错误),gmon.out可能不会生成,或者不完整。

2.3.3 用gprof生成报告

gprof myprogram gmon.out > report.txt

然后打开 report.txt 就能看到分析结果了。报告分两部分:

  • Flat profile(平面分析):每个函数的执行时间和调用次数
  • Call graph(调用图):函数之间的调用关系和时间传播

2.4 读懂gprof报告:热点函数在哪?

我们来看一个实际例子。假设我有这么一段代码:

#include <stdio.h>
#include <math.h>

void compute(int n) {
    double sum = 0.0;
    for (int i = 0; i < n; i++) {
        sum += sin(i) * cos(i);
    }
}

void process(int iterations) {
    for (int i = 0; i < iterations; i++) {
        compute(10000);
    }
}

int main() {
    process(1000);
    return 0;
}

编译运行后,gprof报告大概长这样:

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
 95.2      2.38     2.38     1000     2.38     2.38  compute
  4.8      2.50     0.12        1   120.00  2500.00  process
  0.0      2.50     0.00        1     0.00     0.00  main

看到没?compute 函数占了95.2%的执行时间。这就是热点函数。你优化它,效果立竿见影。

我的经验:看报告时,先看 self seconds 这一列。它表示函数自身消耗的时间(不包括它调用的子函数)。如果一个函数 self seconds 很高,说明它本身的计算量很大。如果 self seconds 不高但 total seconds 很高,说明它调用了很多耗时的子函数。

2.5 生成调用图:看清函数之间的“债务关系”

gprof的调用图部分,能告诉你每个函数被谁调用了,以及调用链上的时间是怎么传播的。比如上面例子的调用图:

Call graph (explanation follows)


granularity: each sample hit covers 2 byte(s) for 0.40% of 2.50 seconds

index % time    self  children    called     name
                0.12    2.38    1000/1000        process [2]
[1]    100.0    2.38    0.00    1000         compute [1]
-----------------------------------------------
                0.00    0.12       1/1           main [3]
[2]      4.8    0.12    2.38       1         process [2]
                2.38    0.00    1000/1000        compute [1]
-----------------------------------------------
                0.00    0.00       1/1           <spontaneous>
[3]      0.0    0.00    0.00       1         main [3]
                0.12    2.38       1/1           process [2]
-----------------------------------------------

这个图怎么读?我教你一个技巧:看方括号里的数字。每个函数有一个索引号。比如 compute [1],它被 process [2] 调用了1000次。箭头方向是从调用者指向被调用者。

调用图的价值在于:它能帮你发现“间接热点”。比如函数A本身不慢,但它调用了100万次函数B,那B就成了热点。你优化B,A也就快了。

2.6 gprof的局限性:你得知道它哪里不准

gprof虽然好用,但有几个坑你得心里有数:

问题 说明 影响
采样精度有限 gprof基于统计采样,不是精确计时 短函数可能被低估或漏掉
内联函数不显示 被内联的函数不会出现在报告中 你可能找不到真正的热点
多线程支持差 gprof对多线程程序的分析不准确 线程间的时间分配会混乱
需要程序正常退出 异常退出不会生成gmon.out 长时间运行的程序不好用

我曾经踩过的坑:有一次我优化一个多线程服务器,用gprof分析后发现某个锁函数占了80%的时间。我花了两天去优化那个锁,结果性能一点没提升。后来才发现,gprof在多线程下把等待时间也算到了锁函数头上,实际上瓶颈根本不在这。所以记住——多线程程序别用gprof,换perf或者Valgrind。

2.7 实战技巧:让gprof更好用

这里分享几个我常用的技巧:

  • 控制采样粒度:用 GMON_OUT_PREFIX 环境变量可以指定输出文件名,方便多次运行对比。
  • 忽略短函数:用 -n 选项可以设置最小采样时间,过滤掉那些不重要的函数。
  • 生成图形化调用图:gprof的输出可以配合 gprof2dot 工具生成可视化的调用图。命令如下:
gprof myprogram gmon.out | gprof2dot | dot -Tpng -o callgraph.png

这样你就能看到一张漂亮的调用图了。我个人习惯先看图形化的调用图,再去看文本报告。图形能让你一眼看出调用链上的“胖子”在哪。

我的习惯:每次优化前,我都会用gprof生成一份基线报告。优化后再跑一次,对比两次的 flat profile。如果热点函数的占比下降了,说明优化有效。如果没变,那就得重新审视你的优化方向了。

2.8 本章知识体系

下面这张图总结了gprof的核心流程和关键点:

gprof 性能分析流程 第一步:编译 gcc -pg -o prog prog.c 第二步:运行 ./prog → 生成 gmon.out 第三步:分析 gprof prog gmon.out gprof 输出报告 Flat Profile(平面分析) 每个函数的执行时间和调用次数 Call Graph(调用图) 函数调用关系和时间传播 关键注意事项 • 编译和链接都必须加 -pg 选项 • 多线程程序不适用,建议用 perf 或 Valgrind • 内联函数不会出现在报告中,编译时可加 -fno-inline 临时禁用内联

这张图把gprof的使用流程和输出结果串起来了。你照着这个流程走一遍,基本就能上手了。


好了,gprof的基础用法就聊到这。它虽然有些局限,但作为入门级的性能分析工具,性价比很高。你花10分钟跑一遍,就能知道程序的大致瓶颈在哪。下一轮,我们再聊聊怎么用这些数据去指导实际的优化工作。