3、Perf基础:perf stat、perf record、perf report的基本用法,理解硬件计数器与采样原理

好,咱们今天聊聊 Perf。这玩意儿是我在嵌入式调优路上用得最顺手的工具之一。说实话,刚入行那会儿我总觉得性能分析是玄学,直到 Perf 把硬件计数器摆在我面前,我才发现——原来 CPU 内部藏着这么多秘密。

3.1 硬件计数器:CPU 自带的“黑匣子”

先说说硬件计数器。你想想看,现代 CPU 内部其实有一堆专门的寄存器,它们默默记录着各种事件:执行了多少条指令、发生了多少次缓存未命中、分支预测失败了几回……这些就是硬件计数器。

我习惯把它们比作飞机的黑匣子。平时不显山不露水,但一旦系统出问题,翻出来一看,问题根源一目了然。

关键点:硬件计数器是 CPU 原生支持的,几乎零开销。你不需要改代码,不需要插桩,直接读寄存器就行。

常见的硬件事件包括:

  • cycles:CPU 时钟周期数
  • instructions:执行的指令数
  • cache-misses:缓存未命中次数
  • branch-misses:分支预测失败次数
  • context-switches:上下文切换次数

嗯,这里要注意:不同架构的 CPU 支持的事件不一样。ARM 和 x86 的计数器列表差别挺大。我在项目中遇到过,把 x86 上的 perf 脚本直接扔到 ARM 板子上跑,结果一堆事件报错——白折腾半天。

3.2 perf stat:看一眼系统的“体检报告”

perf stat 是最简单的用法。说白了,就是让 Perf 跑一下你的程序,然后告诉你各种硬件计数器的统计值。

基本用法:

perf stat ./my_program

输出大概长这样:

 Performance counter stats for './my_program':

          1,234,567      cycles
            890,123      instructions
             12,345      cache-misses
              1,234      branch-misses

       0.123456789 seconds time elapsed

看到这些数字,你能得到什么信息?

  • IPC(每周期指令数) = instructions / cycles。这个值越高,说明 CPU 利用率越好。一般嵌入式场景 IPC 在 0.5~2.0 之间都算正常。
  • 缓存命中率 = 1 - (cache-misses / cache-references)。低于 90% 就要警惕了。
  • 分支预测成功率 = 1 - (branch-misses / branches)。低于 95% 说明代码分支逻辑有问题。

我的习惯:每次调优前先跑一次 perf stat 作为基线。改完代码再跑一次,对比数字变化。这样心里有底,不会瞎调。

还可以指定事件:

perf stat -e cycles,instructions,cache-misses ./my_program

我曾经在调试一个视频解码程序时,发现 cache-misses 异常高。用 perf stat 一查,原来是数据结构布局不合理,导致频繁缓存未命中。调整后性能提升了 30%。

3.3 perf record & perf report:采样分析,找到“热点”

perf stat 只能告诉你整体情况。但问题往往出在某个函数甚至某行代码上。这时候就需要 perf record 了。

它的原理是采样。CPU 每隔一段时间(比如每 1ms)中断一次,记录当前正在执行的指令地址。采样次数多了,就能统计出哪些代码占用的 CPU 时间最多。

基本用法:

perf record ./my_program

运行结束后,会生成一个 perf.data 文件。然后用 perf report 查看:

perf report

输出是一个交互式界面,按 CPU 占用率排序显示各个函数:

Samples: 1K of event 'cycles'
Event count (approx.): 123456789

Overhead  Command   Shared Object     Symbol
  45.23%  my_prog   my_prog          [.] decode_frame
  22.11%  my_prog   my_prog          [.] memcpy
  10.05%  my_prog   libc-2.31.so     [.] __memcpy_avx_unaligned
   5.67%  my_prog   my_prog          [.] filter_pixel
   ...

看到没?decode_frame 函数占了 45% 的 CPU 时间。这就是所谓的“热点”。

注意:采样是概率性的。采样频率太低或者程序运行时间太短,结果可能不准确。我建议采样时间至少 5 秒以上,采样频率用默认值就行。

想看更细的粒度?加 -g 参数生成调用链:

perf record -g ./my_program
perf report -g

这样你就能看到每个热点函数的调用路径。比如 decode_frame 是被谁调用的?是 main 直接调用的,还是经过了好几层封装?

我曾经在优化一个网络协议栈时,发现 memcpy 占用特别高。用 perf report -g 一看,原来是在数据包拷贝时频繁调用。后来改成零拷贝技术,性能直接翻倍。

3.4 采样原理:为什么它能“猜”得这么准?

你可能会问:采样真的靠谱吗?万一采到的都是边缘情况呢?

嗯,这个问题问得好。采样基于一个统计原理:大数定律。只要采样次数足够多,样本分布就能近似反映真实分布。

举个例子:假设你的程序 80% 的时间在跑函数 A,20% 的时间在跑函数 B。采样 1000 次,理论上会有 800 次落在 A 上,200 次落在 B 上。虽然每次采样有随机性,但整体趋势是准确的。

Perf 的采样模式有两种:

  • 周期采样:每 N 个 CPU 周期采样一次。适合分析 CPU 密集型任务。
  • 事件采样:每发生 M 次指定事件(比如缓存未命中)采样一次。适合分析特定瓶颈。

我个人更常用周期采样。因为大多数性能问题最终都归结为“CPU 时间花在哪了”。

避坑指南:我曾经在调试一个多线程程序时,发现 perf report 显示某个函数占用很高,但实际代码逻辑很简单。后来才发现,是线程锁竞争导致 CPU 空转。这种情况下,采样到的“热点”其实是锁等待,而不是真正的计算。所以,看结果时一定要结合代码逻辑分析。

3.5 实战小技巧:快速定位性能瓶颈

最后分享几个我常用的套路:

  1. 先 stat 后 record:先用 perf stat 看整体指标,确认问题方向(CPU 瓶颈?缓存瓶颈?IO 瓶颈?)。再用 perf record 精确定位。
  2. 关注 IPC:IPC 低于 0.5 说明 CPU 在大量等待(比如内存访问延迟)。IPC 高于 2.0 说明代码很高效。
  3. 对比优化前后:改代码前跑一次 perf stat 保存结果,改完后对比。数字说话,最客观。
  4. 别只看百分比:有时候一个函数只占 5%,但它是被调用了 100 万次。优化它可能比优化一个占 50% 但只调用 10 次的函数更有效。

好了,Perf 的基础用法就这些。下一章咱们聊聊怎么用 Perf 分析实际嵌入式系统中的问题——比如中断延迟、调度延迟这些头疼的东西。

一句话总结:perf stat 看全局,perf record 抓细节,硬件计数器是真相。用好这三板斧,性能调优不再玄学。