15. 性能分析工具(上):gprof使用、函数调用图、热点函数识别

性能调优这件事,说白了就是“先找到瓶颈,再动手优化”。

我见过太多人上来就撸袖子改代码,改完发现性能没提升,反而更慢了。为什么?因为你根本不知道程序的时间花在哪了。今天我们要聊的 gprof,就是帮你回答这个问题的工具。

15.1 为什么要用性能分析工具?

你想想看,一个程序跑起来,CPU 在忙什么?是某个循环在空转?还是某个函数调用次数太多?又或者是内存分配拖了后腿?

没有工具,你只能靠猜。靠猜的结果,往往就是优化了 10% 的代码,却忽略了 90% 的热点。

我个人习惯是:先 profile,再优化。哪怕只是改一行代码,我也要先看看改动前后的 profile 数据有没有变化。

核心原则: 不要优化你不知道的东西。性能分析工具就是你的“CT 机”。

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% 的时间,这就是你的 热点函数。优化它,效果立竿见影。

我的经验: 看 flat profile 时,先看 % time 最高的前 3 个函数。如果某个函数占了 80% 以上,别犹豫,它就是你要优化的目标。

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 的编号是 1
  • self 是自身耗时,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 万次。真正的瓶颈不是函数本身,而是那个循环里做了很多不必要的重复计算。我把循环逻辑优化后,性能直接翻倍。

教训: 别只看 flat profile。热点函数可能只是“受害者”,真正的“凶手”是它的调用者。一定要结合 call graph 分析。

另外,gprof 有几个局限性你要知道:

  • 不支持多线程程序(每个线程的采样会混在一起)
  • 对内联函数不友好(内联后函数名可能消失)
  • 对动态库支持有限

如果你的程序是多线程的,建议用 perf 或 Valgrind 的 callgrind。这个我们后面会讲。

15.7 知识体系图

下面这张图帮你理清 gprof 的使用流程和核心概念:

gprof 性能分析流程 编译时加 -pg 选项 运行程序,生成 gmon.out gprof 解析,生成报告 Flat Profile (函数耗时排名) Call Graph (函数调用关系) 识别热点函数 分析调用链

15.8 小结

gprof 虽然老,但胜在简单、轻量、零依赖。对于单线程程序,它依然是快速定位热点的好帮手。

记住三个关键点:

  • 编译加 -pg,运行出 gmon.out
  • flat profile 找热点,call graph 找原因
  • 别只盯着一个函数,要看调用链

下一节我们会聊 gprof 的进阶用法,以及它和 perf 的对比。嗯,到时候再细说。