15. 性能分析工具(上):gprof使用、函数调用图、热点函数识别
性能调优这件事,说白了就是“先找到瓶颈,再动手优化”。
我见过太多人上来就撸袖子改代码,改完发现性能没提升,反而更慢了。为什么?因为你根本不知道程序的时间花在哪了。今天我们要聊的 gprof,就是帮你回答这个问题的工具。
15.1 为什么要用性能分析工具?
你想想看,一个程序跑起来,CPU 在忙什么?是某个循环在空转?还是某个函数调用次数太多?又或者是内存分配拖了后腿?
没有工具,你只能靠猜。靠猜的结果,往往就是优化了 10% 的代码,却忽略了 90% 的热点。
我个人习惯是:先 profile,再优化。哪怕只是改一行代码,我也要先看看改动前后的 profile 数据有没有变化。
15.2 gprof 是什么?
gprof 是 GNU 工具链自带的性能分析工具。它不需要你改代码,只需要在编译时加一个 -pg 选项,运行程序后就会生成一个 gmon.out 文件。然后用 gprof 命令解析这个文件,就能看到函数调用关系和每个函数的耗时。
它的工作原理其实很简单:
- 编译器在编译时,在每个函数入口插入一段计数代码
- 程序运行时,这些计数代码会记录每个函数被调用了多少次
- 同时还会采样程序计数器(PC),统计每个函数占用的 CPU 时间
嗯,这里要注意:gprof 是 统计性 的,不是精确的。它通过采样来估算时间,所以对于非常短的小函数,数据可能不太准。但用来找热点,足够了。
15.3 怎么用 gprof?三步走
第一步:编译时加 -pg
gcc -pg -o myapp myapp.c
如果你有多个源文件,每个文件都要加 -pg。链接时也要加。
-pg 后,程序运行速度会变慢,因为多了计数开销。但这是正常的,不影响分析结果。
第二步:运行程序
./myapp
运行结束后,当前目录下会生成一个 gmon.out 文件。如果程序异常退出,可能不会生成这个文件,所以尽量让程序正常跑完。
第三步:用 gprof 分析
gprof myapp gmon.out > report.txt
打开 report.txt,你会看到两个主要部分:
- Flat profile:每个函数的耗时排名
- Call graph:函数调用关系图
15.4 读懂 Flat Profile:热点函数识别
Flat profile 是 gprof 输出的第一部分,也是最直接的部分。它告诉你每个函数花了多少时间。
举个例子,假设我们有一个程序:
#include <stdio.h>
void slow_function() {
for (int i = 0; i < 100000000; i++);
}
void fast_function() {
for (int i = 0; i < 1000; i++);
}
int main() {
slow_function();
fast_function();
return 0;
}
编译运行后,gprof 输出的 flat profile 大概长这样:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
99.9 0.99 0.99 1 990.00 990.00 slow_function
0.1 1.00 0.01 1 10.00 10.00 fast_function
0.0 1.00 0.00 main
关键列的含义:
| 列名 | 含义 |
|---|---|
| % time | 该函数占用的 CPU 时间百分比 |
| self seconds | 该函数自身消耗的时间(不含子函数) |
| calls | 被调用的次数 |
| ms/call | 每次调用的平均耗时 |
看到 slow_function 占了 99.9% 的时间,这就是你的 热点函数。优化它,效果立竿见影。
15.5 读懂 Call Graph:函数调用图
Flat profile 告诉你“谁最慢”,但没告诉你“谁调用了它”。Call graph 就是干这个的。
还是上面的例子,call graph 部分会输出:
Call graph (explanation follows)
granularity: each sample hit covers 2 byte(s) for 1.00% of 1.00 seconds
index % time self children called name
0.99 0.00 1/1 main [2]
[1] 99.0 0.99 0.00 1 slow_function [1]
-----------------------------------------------
0.01 0.00 1/1 main [2]
[2] 1.0 0.01 0.00 1 fast_function [3]
-----------------------------------------------
0.00 0.00 main [2]
怎么看?
[1]是函数编号,slow_function的编号是 1self是自身耗时,children是子函数耗时called列显示调用关系:1/1 main [2]表示被 main 调用了 1 次
说白了,call graph 就是一张调用关系网。你可以顺着它找到:
- 哪个函数调用了热点函数?
- 热点函数又调用了哪些子函数?
- 调用链上有没有不必要的中间层?
15.6 实战中的避坑指南
我曾经在一个项目中,用 gprof 分析一个图像处理程序。flat profile 显示一个 pixel_convert 函数占了 60% 的时间。我二话不说就开始优化这个函数,改算法、用 SIMD 指令,折腾了两天,性能只提升了 5%。
后来仔细看 call graph 才发现,pixel_convert 被一个外层循环调用了 100 万次。真正的瓶颈不是函数本身,而是那个循环里做了很多不必要的重复计算。我把循环逻辑优化后,性能直接翻倍。
另外,gprof 有几个局限性你要知道:
- 不支持多线程程序(每个线程的采样会混在一起)
- 对内联函数不友好(内联后函数名可能消失)
- 对动态库支持有限
如果你的程序是多线程的,建议用 perf 或 Valgrind 的 callgrind。这个我们后面会讲。
15.7 知识体系图
下面这张图帮你理清 gprof 的使用流程和核心概念:
15.8 小结
gprof 虽然老,但胜在简单、轻量、零依赖。对于单线程程序,它依然是快速定位热点的好帮手。
记住三个关键点:
- 编译加
-pg,运行出gmon.out - flat profile 找热点,call graph 找原因
- 别只盯着一个函数,要看调用链
下一节我们会聊 gprof 的进阶用法,以及它和 perf 的对比。嗯,到时候再细说。