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的使用流程和输出结果串起来了。你照着这个流程走一遍,基本就能上手了。
好了,gprof的基础用法就聊到这。它虽然有些局限,但作为入门级的性能分析工具,性价比很高。你花10分钟跑一遍,就能知道程序的大致瓶颈在哪。下一轮,我们再聊聊怎么用这些数据去指导实际的优化工作。