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 的核心工作流程和数据流向,帮你建立整体认知。

Gprof 性能剖析流程 源代码 gcc -pg 编译 插桩后的程序 运行时执行 gmon.out gprof 解析 Flat Profile Call Graph 调用图可视化 核心 输出

常见陷阱与避坑指南

  • 优化编译选项会干扰结果: 如果开了 -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,把剩下的问题一个个揪出来。