4、Perf实战:采样与分析:perf record与perf report、生成火焰图、定位热点函数

好,咱们进入正题。Perf 这个工具,说白了就是 Linux 系统自带的性能剖析神器。你不需要装任何第三方软件,内核里就给你准备好了。我当年刚接触 ADAS 系统时,遇到 CPU 飙高、任务超时,第一反应就是上 Perf。它就像一把手术刀,能精准切开你的代码,告诉你哪里在“吃”性能。

4.1 核心命令:perf record 与 perf report

先讲最基础的两个命令:perf recordperf report。它们俩是搭档,一个负责采样,一个负责分析。

perf record:采样你的程序

我个人习惯,先跑一个简单的采样看看全局。比如你的 ADAS 感知进程叫 adas_perception,PID 是 1234,你可以这样采样 10 秒:

sudo perf record -p 1234 -g -- sleep 10

解释一下参数:

  • -p 1234:指定要采样的进程 PID。
  • -g:记录调用栈。这个很关键,没有它你就不知道函数是谁调用的。
  • -- sleep 10:采样持续 10 秒。

采样完成后,当前目录会生成一个 perf.data 文件。嗯,这里要注意,如果你在嵌入式板子上跑,磁盘空间可能有限,采样时间别设太长,我一般先试 5 秒看看数据量。

perf report:解读采样结果

采样完,直接敲:

sudo perf report -g

你会看到一个交互式的界面,按百分比排序,显示哪些函数占用的 CPU 时间最多。我遇到过最典型的情况:一个看起来不起眼的 memcpy 函数,居然占了 30% 的 CPU。为什么?因为图像数据在内存里来回拷贝,没做零拷贝优化。

关键指标解读:

  • Overhead(开销百分比):这个函数占用的 CPU 采样比例。超过 10% 就要重点关注。
  • Symbol(符号名):函数名。如果显示为 [unknown],说明没有符号表,编译时记得加 -g 选项。
  • Children(子函数开销):包括该函数及其所有子函数的开销。这个值能帮你找到“调用链”上的瓶颈。

4.2 生成火焰图:一眼看出性能瓶颈

说实话,perf report 的文本界面虽然能用,但不够直观。我个人强烈推荐生成火焰图。火焰图就像一张热力图,颜色越深、越宽的地方,就是性能热点。

生成火焰图需要两个脚本:stackcollapse-perf.plflamegraph.pl。你可以从 Brendan Gregg 的 GitHub 上下载。步骤很简单:

# 第一步:将 perf.data 转换成折叠的调用栈
sudo perf script -i perf.data > out.perf

# 第二步:折叠调用栈
./stackcollapse-perf.pl out.perf > out.folded

# 第三步:生成 SVG 火焰图
./flamegraph.pl out.folded > flame.svg

打开 flame.svg,你会看到一张从下往上的“火焰山”。底部是根函数(比如 main),顶部是叶子函数(比如 memcpy)。

我的读图技巧:

  • 先看顶部最宽的“平顶山”,那就是最耗 CPU 的函数。
  • 再看“烟囱”形状,如果某个函数调用栈特别深、特别窄,说明它被频繁调用但执行很快,可能不是主要问题。
  • 重点关注那些“又宽又高”的矩形,它们才是性能杀手。

我曾经在调试一个车道线检测算法时,发现火焰图上有一个巨大的 cv::warpAffine 函数块。说白了,就是图像仿射变换太频繁了。后来我改成只在 ROI 区域做变换,CPU 占用直接降了 40%。

4.3 定位热点函数:从火焰图到代码行

火焰图告诉你哪个函数是热点,但还不够。你还需要知道是函数里的哪一行代码在“烧 CPU”。这时候就要用 perf annotate 了。

假设火焰图显示 process_image 函数占用了 25% 的 CPU,我们想看看它内部的情况:

sudo perf annotate -s process_image

你会看到类似这样的输出:

Percent |  Source code & Disassembly
------------------------------------
 12.5   |  for (int i = 0; i < width; i++) {
  8.3   |      for (int j = 0; j < height; j++) {
  4.2   |          pixel = src[i * height + j];
  0.0   |          dst[j * width + i] = pixel;
  0.0   |      }
  0.0   |  }

看到了吗?内层循环的 pixel = src[i * height + j] 这一行占了 4.2% 的 CPU。为什么?因为每次都要计算 i * height,而且内存访问不连续,导致缓存 miss。

避坑指南:

我曾经在项目中遇到过一个问题:perf annotate 显示的百分比和 perf report 对不上。后来发现是因为编译器做了内联优化。你看到的 process_image 函数,实际上可能被内联到了调用方。解决办法是编译时加上 -fno-inline 选项,或者用 --no-inline 参数让 Perf 不展开内联函数。

4.4 实战案例:ADAS 感知模块的 CPU 优化

讲一个我实际处理过的案例。某款 ADAS 系统在高速场景下,感知模块的 CPU 占用率飙到了 85%,导致规划模块频繁超时。

我做了三步:

  1. 全局采样perf record -g -p [PID] -- sleep 5,生成火焰图。
  2. 定位热点:火焰图显示 feature_extraction 函数占了 45% 的 CPU,其中 calc_gradient 子函数占了 30%。
  3. 代码级分析perf annotate -s calc_gradient,发现瓶颈在 sqrt(gx*gx + gy*gy) 这一行,占 18%。

优化方案很简单:把 sqrt 换成查表法,因为梯度幅值不需要那么高的精度。改完后,calc_gradient 的 CPU 占用从 30% 降到了 8%,整个感知模块的 CPU 占用降到了 55%。

优化前后对比:

函数名 优化前 CPU 占比 优化后 CPU 占比 优化手段
calc_gradient 30% 8% sqrt 查表法
feature_extraction 45% 22% 整体优化
感知模块总计 85% 55% -

你想想看,如果没有 Perf 和火焰图,我可能还在盲目地优化 for 循环,根本不知道问题出在 sqrt 上。这就是工具的价值。

4.5 总结与建议

最后,给你几个实用建议:

  • 采样时长要合理:嵌入式板子采样 3-5 秒就够了,别贪多。数据太大分析起来也慢。
  • 一定要加 -g 参数:没有调用栈的火焰图,就像没有地图的导航,你根本不知道路怎么走。
  • 多跑几次:单次采样可能有偶然性。我一般跑 3 次,取平均值,或者看哪个热点函数每次都出现。
  • 结合源码看:火焰图告诉你“哪里热”,源码告诉你“为什么热”。两者缺一不可。

嗯,Perf 实战就讲到这里。下一章我们会深入 Trace 工具,看看怎么分析系统级的调度延迟和中断响应。准备好了吗?