Perf工具详解:perf stat、perf record、perf report的基本用法,硬件计数器解读

性能分析这件事,我做了十几年。说实话,最让我头疼的不是代码写得烂,而是不知道烂在哪里。你优化了半天,结果发现瓶颈根本不在你改的那块——这种挫败感,我相信很多人都体会过。

Perf 这个工具,是我个人最常用的性能分析利器。它不像一些商业工具那样花里胡哨,但胜在直接、精准、零开销(几乎)。今天我就带你把它吃透。

核心观点:Perf 不是万能的,但没有 Perf,你就像在黑暗中摸索。它让你看到 CPU 到底在忙什么,而不是你以为它在忙什么。

一、perf stat:看一眼程序的“体检报告”

perf stat 是最简单的子命令。说白了,就是给程序做个快速体检。你不需要改代码,不需要插桩,直接跑就行。

# 基本用法
perf stat ./my_program

# 输出示例
Performance counter stats for './my_program':

    1,234.56 msec task-clock                #    1.000 CPUs utilized
           123      context-switches        #    0.100 K/sec
            12      cpu-migrations          #    0.010 K/sec
         5,678      page-faults             #    0.004 M/sec
 3,456,789,012      cycles                  #    2.800 GHz
 2,345,678,901      instructions            #    0.68  insn per cycle
   456,789,012      branches                #  370.123 M/sec
     4,567,890      branch-misses           #    1.00% of all branches

这里我重点说几个我经常看的指标:

  • instructions per cycle (IPC):这个值低于 0.5 就要警惕了。说明 CPU 在大量空转,可能是缓存 miss 或者分支预测失败。
  • branch-misses:超过 5% 就算高了。我在项目中遇到过一段排序代码,分支预测失败率高达 15%,换成分支友好的算法后,性能直接翻倍。
  • context-switches:如果这个值很高,说明你的程序在频繁让出 CPU。可能是锁竞争,也可能是 I/O 阻塞。

我的习惯:每次优化前后,我都会跑一次 perf stat,把关键指标记下来。这样能直观看到优化效果,而不是凭感觉说“好像快了”。

二、perf record + perf report:深入“案发现场”

perf stat 只能看整体情况。但问题往往藏在细节里。这时候就需要 perf record 了。

perf record 的原理很简单:它让 CPU 每隔一段时间(比如每 1000 个周期)中断一次,记录当前正在执行什么函数。采样次数足够多,就能还原出程序的“热点分布”。

# 采样 10 秒
perf record -a -g -- sleep 10

# 或者针对特定程序
perf record -g ./my_program

# 生成报告
perf report

这里 -g 参数很重要,它开启调用链记录。没有它,你只能看到哪个函数最热,但不知道是谁调了它。

我曾经帮一个团队排查性能问题。他们用 perf record 采样后发现,一个叫 parse_config 的函数占了 40% 的 CPU。但奇怪的是,这个函数只会在启动时调用一次。后来加上 -g 才发现,原来是某个循环里反复调用了这个函数——一个典型的“缓存结果”就能解决的问题。

注意:perf record 会生成 perf.data 文件,可能很大。采样时间越长,文件越大。我建议先短时间采样(比如 5 秒),看看热点是否稳定,再决定要不要加长时间。

三、硬件计数器解读:看懂 CPU 在“想什么”

perf 的强大之处在于它能直接读取 CPU 内部的硬件计数器。这些计数器是 CPU 自带的,几乎零开销。你想想看,这比那些靠插桩的工具有多大优势。

常用的硬件事件包括:

事件名称 含义 我常用的场景
cycles CPU 周期数 衡量程序整体耗时
instructions 执行的指令数 结合 cycles 算 IPC
cache-misses L1/L2/L3 缓存未命中 排查内存访问瓶颈
branch-misses 分支预测失败 优化条件分支
stalled-cycles-frontend 前端停顿(取指令阶段) 代码体积过大导致指令缓存 miss
stalled-cycles-backend 后端停顿(执行阶段) 数据依赖或缓存 miss

怎么用呢?举个例子:

# 只看缓存相关事件
perf stat -e cache-references,cache-misses ./my_program

# 自定义事件
perf stat -e cycles,instructions,branch-misses,L1-dcache-load-misses ./my_program

我记得有一次,一个网络转发程序性能上不去。perf stat 显示 IPC 只有 0.3,但指令数并不高。我加了 stalled-cycles-backend 一看,发现后端停顿占了 60%。再深入看,是大量的 cache-misses。最后发现是数据结构设计不合理,频繁跨 NUMA 节点访问内存。改成 per-CPU 数据结构后,性能提升了 3 倍。

关键思路:不要只看一个指标。比如 cache-misses 高,不一定就是坏事——如果你的数据本来就不在缓存里,那 miss 是正常的。要结合 IPC、stalled-cycles 一起看,才能定位真正的瓶颈。

四、实战:用 perf 定位一个真实瓶颈

光说不练假把式。我模拟一个场景:

假设你有一个排序程序,跑得很慢。你怀疑是排序算法本身的问题,但不确定。

// 一个简单的冒泡排序
void bubble_sort(int *arr, int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

第一步,跑 perf stat:

perf stat ./sort_program

输出显示 branch-misses 高达 12%,IPC 只有 0.4。这说明分支预测失败很严重。

第二步,跑 perf record:

perf record -g ./sort_program
perf report

报告显示,bubble_sort 占了 95% 的 CPU 时间。但有意思的是,if (arr[j] > arr[j + 1]) 这一行本身并不复杂,问题出在 branch-misses 上。

为什么会这样?因为冒泡排序的内层循环中,arr[j] > arr[j + 1] 这个比较结果几乎是随机的——数据越无序,分支预测越难猜对。CPU 每次猜错都要清空流水线,代价很大。

解决方案?换成分支友好的排序算法,比如归并排序或者基数排序。或者,如果数据范围有限,用计数排序直接消除分支。

避坑指南:我曾经以为“优化分支”就是减少 if-else 的数量。后来发现,分支的可预测性比数量更重要。一个可预测的分支,即使出现很多次,代价也很小;一个不可预测的分支,出现一次就够你受的。

五、知识体系:一张图看懂 Perf 工具链

说了这么多,我画了一张图,帮你把 Perf 的核心逻辑串起来:

Perf 工具链核心逻辑 目标程序 perf stat(整体统计) perf record(采样记录) 硬件计数器汇总 perf.data(采样数据) perf report(热点分析) CPU 硬件计数器(零开销) perf stat 看整体 → perf record 抓细节 → perf report 定位热点

这张图想表达的是:perf stat 和 perf record 是两条互补的路径。stat 告诉你“哪里有问题”,record 告诉你“问题具体在哪”。两者结合,才能高效定位性能瓶颈。


好了,Perf 工具的核心用法就讲到这里。记住,工具只是手段,关键是你要有“性能意识”——看到一段代码,能大致猜到它会在哪些硬件计数器上表现不佳。这种能力,只能靠多练、多分析来积累。

下次你遇到性能问题,别急着改代码。先跑一遍 perf stat,看看数据怎么说。

一句话总结:perf stat 是体检,perf record 是 CT 扫描,硬件计数器是医生的诊断指标。三者结合,没有看不透的性能问题。

公众号:蓝海资料掘金营,微信deep3321