2. 嵌入式系统基础
性能剖析,说白了就是给代码做「体检」。你得知道哪里慢、哪里耗电、哪里在空转。Gprof 是我最早接触的 GNU 工具链性能分析器,虽然现在有更花哨的工具,但 Gprof 的「代码插桩 + 运行时采样」思路,至今仍是嵌入式优化的基本功。
我记得刚入行那会儿,接手一个跑在 ARM9 上的数据采集程序,总觉得响应慢。直觉告诉我「可能是中断处理太频繁」,结果 Gprof 一跑,真相让我大跌眼镜——最耗时的居然是一个字符串处理函数。嗯,从那以后我再也不敢凭感觉优化了。
Gprof 的安装:别踩平台坑
Gprof 是 GNU binutils 的一部分。在 Linux 或嵌入式 Linux 环境下,通常已经预装了。但交叉编译环境里,你得确认目标工具链是否带了 gprof 支持。
快速检查命令:
# 检查本机是否支持
gprof --version
# 检查交叉工具链
arm-linux-gnueabihf-gprof --version
如果提示找不到,别慌。重新编译 binutils 时加上 --enable-gprof 就行。我个人习惯在构建交叉工具链时,把 gprof、gcov 这些分析工具一并打开,省得后面再折腾。
注意: 有些轻量级嵌入式 Linux 发行版(比如 Buildroot 的最小配置)默认不包含 gprof。你得在配置菜单里勾上 BR2_PACKAGE_GPROF。我曾经在一个只有 4MB Flash 的项目里忘了这茬,编译出来的程序跑不了 gprof,最后只能重新刷固件。
代码插桩:Gprof 的核心机制
Gprof 的工作原理其实不复杂。编译时加个 -pg 选项,编译器就会在每个函数入口插入一段计数代码。运行时,这些计数器记录每个函数被调用了多少次、花了多少时间。程序退出时,数据写入 gmon.out 文件。
你想想看,这相当于给每个函数装了个「打卡机」。每次进入函数就打卡一次,退出时再打一次。最后汇总成一张考勤表。
编译与运行示例
# 编译时加 -pg 选项
gcc -pg -o my_program my_program.c
# 运行程序(正常执行即可)
./my_program
# 查看生成的剖析数据
ls -l gmon.out
小技巧: 如果程序运行时间太短(比如几毫秒就结束),gprof 的采样精度会不够。我一般会在被测函数外面加一个循环,让程序跑至少 1-2 秒,这样采样数据才够密。当然,正式测试时记得去掉这个循环。
生成调用图与性能报告
拿到 gmon.out 之后,用 gprof 命令解析它。输出两份核心报告:flat profile(平面剖析)和 call graph(调用图)。
# 生成文本报告
gprof my_program gmon.out > report.txt
# 或者直接看标准输出
gprof my_program gmon.out
我个人习惯把报告重定向到文件,然后用 less 或 vim 慢慢翻。终端直接看容易眼花。
解读 Flat Profile:谁在偷时间?
Flat profile 是一张扁平的表格,按函数耗时从高到低排序。它回答一个核心问题:CPU 时间都花在哪些函数里了?
| 列名 | 含义 | 关注点 |
|---|---|---|
| % time | 该函数占用的 CPU 时间百分比 | 超过 20% 就要警惕 |
| cumulative seconds | 累计到当前行的总秒数 | 看前几行就能定位热点 |
| self seconds | 函数自身执行时间(不含子函数) | 优化重点:自身耗时高的函数 |
| calls | 函数被调用的次数 | 调用次数异常多可能是问题 |
| self ms/call | 每次调用的自身耗时 | 微秒级函数不用太纠结 |
实战经验: 我一般先看 self seconds 最高的前 3 个函数。如果某个函数自身耗时占比超过 30%,那它就是优化突破口。但要注意——有些函数自身耗时不高,却被调了上万次,积少成多也很可怕。
解读 Call Graph:谁在调用谁?
Call graph 展示的是函数调用关系树。它比 flat profile 多一层信息:每个函数的耗时,有多少是它自己消耗的,有多少是它调用的子函数消耗的。
举个例子,假设函数 A 耗时 100ms,其中 20ms 是 A 自己的代码,80ms 是 A 调用了 B。如果你只优化 A 自身,最多省 20ms;但如果发现 B 效率低,优化 B 可能省 80ms。这就是 call graph 的价值。
index % time self children called name
[1] 95.0 0.02 0.93 1 main [1]
0.01 0.50 1 process_data [2]
0.01 0.42 1 write_output [3]
-----------------------------------------------
[2] 51.0 0.01 0.50 1 process_data [2]
0.30 0.00 10000 strcmp [4]
0.10 0.10 10000 malloc [5]
-----------------------------------------------
看到上面这个片段了吗?process_data 自身只花了 0.01 秒,但它的子函数 strcmp 被调了 10000 次,花了 0.30 秒。优化方向就很明确了——要么减少 strcmp 调用次数,要么换更快的字符串比较方法。
我的习惯: 拿到 call graph 后,先找那些「自身耗时低、但子函数耗时高」的父函数。这种地方往往是「调用次数过多」导致的,优化空间最大。我曾经在一个网络协议栈里发现,一个看似无害的日志函数,因为每次发包都调用,导致整体性能下降 40%。去掉那行日志后,吞吐量直接翻倍。
可视化调用图:SVG 结构
文本报告虽然详细,但不够直观。下面这张 SVG 图展示了 Gprof 的核心工作流程和数据流向,帮你建立整体认知。
常见陷阱与避坑指南
- 优化编译选项会干扰结果: 如果开了
-O2或-O3,编译器可能内联函数、重排代码,导致 gprof 的计数不准。我一般先用-O0 -g -pg做剖析,确认热点后再用优化选项编译正式版本。 - 多线程程序不适用: Gprof 只跟踪主线程。子线程的调用信息不会被记录。如果你做多线程优化,得用 perf 或 Valgrind 的 callgrind。
- 信号处理函数可能漏记: 如果程序频繁处理信号,gprof 的采样可能被中断。我曾经在一个实时控制程序里发现 gprof 报告的时间总和不到实际运行时间的一半,就是因为信号处理占用了大量 CPU 却没被统计。
- gmon.out 文件权限: 程序要有写权限才能生成 gmon.out。在嵌入式设备上,如果根文件系统是只读的,记得把工作目录切到可写分区。
重要提醒: Gprof 的计时精度依赖于系统时钟。在嵌入式 Linux 上,如果内核没有开启高精度定时器(CONFIG_HIGH_RES_TIMERS),采样粒度可能只有 10ms。对于微秒级的短函数,数据仅供参考,别太较真。
小结:Gprof 能做什么,不能做什么
Gprof 适合做两件事:定位 CPU 热点函数 和 分析函数调用关系。它不适合做:I/O 等待分析、多线程竞争分析、内存分配热点分析。
说白了,Gprof 是性能优化的「第一把刀」。它帮你快速找到最肥的肉,但切肉还得靠其他工具。下一阶段我们会深入 Valgrind 和 perf,把剩下的问题一个个揪出来。