4. 性能分析工具链:Linux perf 工具在 ARM64 上的使用

性能调优这件事,说白了就是回答三个问题:哪里慢?为什么慢?怎么改? 在 ARM64 平台上,Linux perf 就是回答前两个问题最趁手的家伙。我这些年调过的坑,十有八九都是靠 perf 先定位到的。

perf 是内核自带的性能剖析工具,不需要额外装什么商业软件。它利用 PMU(Performance Monitoring Unit)硬件计数器来采集数据,对目标程序的影响极小。这一点在 ARM64 上尤其重要——ARM 的 PMU 架构和 x86 有些差异,但 perf 已经帮你封装好了。

核心认知: perf 不是万能的,但没有 perf 是万万不能的。它帮你把「感觉程序慢」变成「L1 cache miss 率 12.7%,分支预测错误率 3.2%」这样的量化数据。

4.1 先确认你的环境

在敲 perf 命令之前,先检查三件事:

  1. 内核配置: 确认 CONFIG_PERF_EVENTS=y。大部分发行版默认开了,但嵌入式板子可能没开。
  2. PMU 可用性: 跑一下 perf list,看看能不能列出硬件事件。如果只看到软件事件,说明 PMU 没使能。
  3. 权限问题: 普通用户可能只能看自己的进程。加 sudo 或者设置 kernel.perf_event_paranoid = -1。
# 快速检查环境
uname -r
cat /proc/sys/kernel/perf_event_paranoid
perf list | grep -i "cache\|branch\|cycle" | head -20
注意: 在虚拟化环境(比如 AWS Graviton 实例)里,PMU 计数器可能被限制。我曾经在 KVM 虚拟机里折腾半天,结果发现宿主机没透传 PMU——白忙活一场。

4.2 perf stat:快速看全局

perf stat 是我用得最多的子命令。它跑完一个程序,然后告诉你关键计数器的汇总值。适合做「快照式」对比。

# 基本用法
perf stat ./my_arm64_app

# 指定事件
perf stat -e cycles,instructions,cache-references,cache-misses ./my_arm64_app

# 跑多次取平均
perf stat -r 5 ./my_arm64_app

输出长这样:

 Performance counter stats for './my_arm64_app':

       1,234,567,890      cycles                    #    2.4 GHz
         987,654,321      instructions              #    0.80  insn per cycle
          12,345,678      cache-references          #    1.0% of all cycles
           1,234,567      cache-misses              #   10.0% of all cache refs

       0.512345678 seconds time elapsed

我最关注三个指标:

  • IPC(Instructions Per Cycle): ARM64 上一般 0.5~1.5 算正常。低于 0.5 说明有严重瓶颈。
  • Cache miss 率: 超过 5% 就该看看数据局部性了。
  • 分支预测错误率: 超过 5% 说明分支模式混乱。
我的习惯: 每次优化前后都跑一次 perf stat -r 3,把结果记下来。别信感觉,数据说话。

4.3 perf record & perf report:深入看热点

perf stat 只能看总量,想知道「哪行代码最慢」就得靠 perf record。它会采样程序执行时的 PC(程序计数器),然后生成报告。

# 采样 10 秒
perf record -F 99 -g ./my_arm64_app

# 生成报告
perf report

# 生成火焰图数据
perf script > out.perf

参数说明:

参数 含义 建议值
-F 采样频率(Hz) 99 或 199(避免与系统时钟共振)
-g 记录调用栈 必加,否则看不到函数关系
-p 附加到已有进程 调试后台服务时用
--call-graph dwarf 使用 DWARF 调试信息展开栈 ARM64 上推荐,比 fp 模式更准
避坑: ARM64 的栈回溯比 x86 麻烦。如果 -g 出来的调用栈不完整,试试 --call-graph dwarf。我遇到过 GCC 优化后 fp 寄存器被复用的情况,换成 dwarf 就好了。

4.4 ARM64 特有的事件

ARM 的 PMU 有一些 x86 没有的事件,值得关注:

# 列出 ARM 架构特定事件
perf list | grep -i "arm\|arch\|imx\|exynos"

常见的有:

  • stall_frontend / stall_backend: 前端(取指、解码)还是后端(执行、访存)在等?
  • l1d_cache / l1i_cache: 数据 cache 和指令 cache 分开统计。
  • br_mis_pred: 分支预测错误次数。
  • bus_access: 总线访问次数,反映内存带宽压力。
实战技巧: 如果 stall_backend 很高,说明执行单元在等数据。这时候看 l1d_cache_miss 和 stall_ld_miss,基本能锁定是 cache 问题。

4.5 实战:定位一个 cache 性能问题

假设我们有这么一段代码:

// matrix_mult.c
void matrix_mult(int n, float *a, float *b, float *c) {
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++) {
            float sum = 0.0f;
            for (int k = 0; k < n; k++)
                sum += a[i * n + k] * b[k * n + j];
            c[i * n + j] = sum;
        }
}

编译运行,用 perf 分析:

gcc -O2 -o matrix_mult matrix_mult.c
perf stat -e cycles,instructions,L1-dcache-load-misses ./matrix_mult

输出:

L1-dcache-load-misses: 45,678,901  (23.4% of all L1 loads)

23.4% 的 L1 miss 率,太高了。再用 perf record 看热点:

perf record -e L1-dcache-load-misses -g ./matrix_mult
perf report

报告显示热点在内层循环的 b[k * n + j] 访问。原因很明显:b 是按列访问的,而 C 语言数组按行存储,导致 cache 行利用率极低。

优化方案:交换 j 和 k 循环,或者对 b 做转置。优化后 L1 miss 率降到 2.1%,性能提升 3 倍。

经验之谈: 这种「按列访问大数组」的问题,在 ARM64 上比 x86 更明显。因为 ARM 的 cache line 通常是 64 字节,一次 miss 只换来 16 个 float,浪费严重。

4.6 知识结构图

下面这张图概括了 perf 工具链在 ARM64 上的使用逻辑:

perf 工具链在 ARM64 上的使用逻辑 perf 工具链在 ARM64 上的使用逻辑 perf stat perf record perf list 汇总计数器 IPC / Miss率 / 耗时 采样数据 + 调用栈 热点函数 / 火焰图 可用事件列表 硬件/软件/追踪点 ARM PMU 计数器 DWARF 栈回溯 arch 特定事件 瓶颈定位 热点函数分析 事件筛选 三者配合使用:stat 看全局 → record 挖热点 → list 选事件

4.7 常见问题与避坑

问题 现象 解决办法
采样频率不准 实际采样率远低于 -F 指定值 检查 /proc/sys/kernel/perf_event_max_sample_rate,默认可能只有 1000
调用栈不完整 perf report 里很多 [unknown] 加 --call-graph dwarf,或者编译时加 -fno-omit-frame-pointer
事件计数为 0 perf stat 显示某些硬件事件为 0 确认 PMU 是否使能,检查 dmesg | grep PMU
多核调度干扰 结果每次跑都不一样 用 taskset -c 0 绑定到固定核心,或者 perf stat -a 看全系统
曾经踩过的坑: 我在某款 ARM64 服务器上发现 perf stat 报告的 cycles 比实际时间算出来的还少。查了半天,发现是固件把 PMU 的时钟源设成了低频备用时钟。这种硬件问题,perf 本身无能为力,得找板级支持包的人。

4.8 小结

perf 在 ARM64 上的用法和 x86 基本一致,但要注意三点:栈回溯方式、ARM 特有事件、PMU 硬件可用性。我个人建议的调试流程是:

  1. 先用 perf stat 看 IPC 和 cache miss 率,判断瓶颈类型。
  2. 再用 perf record -g 定位热点函数。
  3. 最后用 perf list 找到对应 ARM 事件,做针对性优化。