跳到内容

18.2 用 perf 建立性能证据

地心旅程快到出口时,控制台只留下一句模糊报告:“程序慢。”如果凭声音猜是 cache、分支还是锁,你可能在最漂亮的代码上浪费一整天。性能观测仪不会替你下结论,但能把猜想变成可以推翻的假设。

Linux perf 同时覆盖 hardware PMU counter、software event 和 tracepoint。它回答两类不同问题:

  • perf stat 统计一段运行里发生了多少事件;
  • perf record/report 通过 sampling 估计事件集中在哪些 instruction 和 call stack。

counter 不是 call stack,sampling 也不是每次事件的完整日志。先写下问题和 workload,再选择模式。

1. 采集前的四项检查

工作负载是否代表问题

让程序运行足够久,覆盖实际慢路径,并保存数据规模、并发和输入分布。只 profile 启动阶段,得到的热点可能全是动态链接和初始化。

二进制是否可符号化

保留独立 debug symbols,记录 build ID,并避免在分析前删除对应二进制。优化构建可以继续使用;-g 不等于 -O0

调用栈还取决于 unwind 方式。frame-pointer、DWARF CFI 和硬件 LBR 各有可用性与开销边界;不存在一组选项适合所有 CPU、语言和生产环境。

当前平台提供哪些事件

bash
perf list

事件名由 CPU PMU、内核和虚拟化暴露能力决定。不要从另一台机器复制 raw event 编码。混合大小核平台还可能有多个 PMU,同名事件需要区分 unit。

权限与作用域

系统配置会限制普通用户采集内核地址、其他进程或 system-wide 数据。容器也可能没有 PMU 访问能力。遇到权限错误时应按组织安全策略授权最小范围,不要直接关闭整机保护。

2. perf stat:先看总账

从默认统计开始:

bash
perf stat -r 5 -- ./application arguments

-r 5 重复运行并报告方差相关信息,适合发现明显不稳定。需要特定事件时,再根据 perf list 选择:

bash
perf stat -e cycles,instructions,branches,branch-misses +  -- ./application arguments

并非所有平台都支持这组名字;失败或显示 <not supported> 时,先确认 PMU 和权限。

怎样读,而不是怎样猜

elapsed time 是端到端时间。user/system time 帮助区分用户态 CPU 与内核态 CPU,但多线程总 CPU 时间可以超过 wall time。

instructions/cycles 可计算 IPC。低 IPC 可能来自依赖链、cache miss、分支错误、前端供给或线程没真正获得 CPU;它不是“内存瓶颈”的单独证明。

branch-misses/branches 描述被计数分支中的预测失败比例。编译器可能已把源码 if 变成条件移动,事件也受 PMU 定义影响。

cache-misses 是宽泛事件,未必对应某一级 cache 或某种 load。要定位 cache 层级、TLB 或远端 NUMA,需要平台专用事件或 memory profiling。

若请求的事件超过硬件计数器数量,perf 会 multiplex。输出中的运行时间比例与缩放值得检查;两个不同时段轮换的事件不适合直接构造高精度比值。

3. perf record:热点在哪里

基本 CPU 采样流程:

bash
perf record --call-graph dwarf -- ./application arguments
perf report

perf record 把样本写入 perf.dataperf report 再按 symbol、DSO 和调用关系聚合。默认采样事件与频率由平台和 perf 版本决定;需要可复现实验时,应显式记录命令、版本和 event。

若构建保留 frame pointer,可以评估:

bash
perf record --call-graph fp -- ./application arguments

frame-pointer unwind 开销较低,但遇到省略 frame pointer 或混合 runtime 时可能得到断裂甚至错误的栈。DWARF 会采集用户栈片段并据 CFI 展开,记录量和开销更高。LBR 只在部分平台可用,也有调用栈深度和组合选项限制。

不要看到栈断了就解释业务。先用已知调用链验证 unwind 质量。

4. Inclusive 与 self overhead

假设:

text
handle_request
  ├─ parse
  └─ query_database
       └─ decode_rows

handle_request 的 inclusive cost 包含所有子调用;self cost 只计算采样点落在自身指令的部分。宽父框不表示父函数本身计算很多,它也可能只是调用了昂贵子函数。

优化时沿最宽调用路径向上追所有权、向下找实际消耗:

  • 顶部 leaf 很宽:成本集中在自身;
  • 中间框宽而 self 小:它是聚合入口;
  • 同一 leaf 出现在多个路径:需要判断哪个调用者贡献更大;
  • [unknown] 或十六进制地址很多:先修符号与 unwind。

采样百分比是对所选 event 的估计,不自动等于 wall-time 比例。采 cyclescpu-clock 和 cache event 会得到不同意义的图。

5. 火焰图是另一种调用栈投影

若已安装相应 stack collapse 与 FlameGraph 脚本,可以:

bash
perf script > samples.perf
stackcollapse-perf.pl samples.perf > samples.folded
flamegraph.pl samples.folded > cpu.svg

读图原则:

  • 横向宽度表示该 frame 出现在多少选定事件样本中;
  • 纵向是调用深度,不是耗时;
  • 横向位置通常不表示时间先后;
  • 默认颜色主要用于视觉区分,不代表温度或严重级别;
  • 顶部宽平台常是实际 on-CPU leaf,底部宽框通常是入口或聚合路径。

火焰图会牺牲时间轴。若问题是周期性抖动、一次短尖峰或启动阶段,时间序列、分窗口 profile 或 trace 往往更合适。

6. CPU 火焰图看不到睡眠时间

线程阻塞在:

  • mutex 或 condition variable;
  • 磁盘与网络 I/O;
  • page fault;
  • scheduler run queue;
  • 下游 RPC;
  • runtime safepoint;

时,它不消耗用户态 CPU,因此 on-CPU 样本不会按 wall time 把等待画宽。

先用业务 trace、线程状态和系统指标判断“运行慢”还是“等待久”。Linux 侧可以根据场景使用 scheduler tracepoint、perf schedperf lock、eBPF 工具或应用内 tracing。具体命令与权限依版本而变,先用本机 help/list 确认可用数据。

off-CPU 分析需要记录阻塞开始与结束,并把等待时间归到相应栈。只采一次睡眠函数调用无法得到等待持续时间。

7. 从 CPU 热点到 cache 与共享

发现循环占据大量 cycles 后,再提出更具体假设:

工作集超过 cache。 随数据规模变化画吞吐曲线,观察是否在某个容量附近出现拐点。

访问缺乏局部性。 比较连续数组、指针追逐或结构体字段拆分后的事件与时间。

伪共享。 不同线程写不同变量,但变量落在同一 coherence unit,cache line 在核心间迁移。

真正共享写。 多线程都修改同一计数或队列头,单纯 padding 无法消除争用,需要分片或改变算法。

perf c2c 在支持的平台上可帮助定位 cache-to-cache sharing。通用 LLC-load-misses 并不能直接证明 false sharing:数据可能来自内存,也可能是正常容量 miss,且不同架构事件语义不同。

修复假共享常采用每线程局部数据,最后聚合;padding/alignment 只有在确认字段确实落在同一 line 且生命周期允许时才合适。缓存行大小与结构布局要从目标平台验证,不要把 64 字节写成 C 语言常量真理。

8. 分支、前端与汇编

若 branch miss 占比可疑:

  1. perf report 找到样本集中的函数;
  2. perf annotate 把源代码和汇编对齐;
  3. 确认源码分支是否仍存在;
  4. 检查输入分布和热点路径布局;
  5. 修改后同时比较 cycles、instructions、branch misses 与总时间。

无分支版本可能执行更多指令,条件移动也会延长依赖链。__builtin_expect 主要帮助编译器做布局和静态决策,不会把概率直接写进硬件预测器。

前端问题还可能来自指令 cache、译码带宽和过度内联。火焰图显示某个函数宽,不代表继续内联它就更快;代码体积膨胀可能让热路径互相挤出 instruction cache。

9. 采样偏差与测量开销

采样不是无扰动观察:

  • 频率过高会增加中断、记录与 unwind 开销;
  • buffer 太小会丢样本;
  • 短函数可能因 skid 或事件精度归到邻近指令;
  • 动态生成代码需要正确的 JIT symbol 支持;
  • 容器 PID/mount namespace 会影响 symbol 路径解析;
  • system-wide profile 可能收集敏感调用栈与地址;
  • 程序相位变化会让整段聚合掩盖局部问题。

先在测试环境评估开销。生产采集应限制持续时间、频率、事件、目标 PID/cgroup 和数据访问权限,并遵守隐私与运维流程。

对比两个版本时使用相同采样方法。若优化幅度接近 profiler 自身扰动或运行方差,结论不够稳。

10. 从“程序慢”走到可复查的证据链

观测仪读数很多,排障却不该从随机 event 开始。先固定 workload 和症状,再从总账缩小到热点、调用栈、cache 或调度证据;每走一步,都要能解释下一条命令回答什么问题。

text
SLO 变差

  ├─ CPU 饱和?
  │    ├─ 是:perf stat → record/report → annotate
  │    └─ 否:看排队、锁、I/O、下游和 runtime

  ├─ 热点受什么限制?
  │    ├─ 指令/算法
  │    ├─ branch/front-end
  │    ├─ cache/TLB/NUMA
  │    ├─ shared cache line/lock
  │    └─ syscall/device

  └─ 最小改动 → 重跑正确性、SLO、资源与 profile

工具选择跟着假设走,而不是把默认 perf stat 的每一行都解释一遍。一个 counter 异常通常只是下一次实验的入口。

11. 小结

perf 能把性能直觉变成可复核证据:

  • stat 给出一段运行的事件总账;
  • record/report 用采样定位 event 集中的代码与调用路径;
  • frame-pointer、DWARF 和 LBR 是不同的 call graph 获取方式;
  • 火焰图宽度属于所选样本,不是自动的 wall-time 百分比;
  • on-CPU profile 看不到锁、I/O 和排队中的完整等待;
  • cache miss 不自动证明 false sharing,需更具体事件和数据布局证据;
  • 事件、PMU 与权限随机器变化,应从 perf list 和本机文档开始。

至此,第 3 卷从编码、CPU、内存、进程、文件系统一路走到性能验证。下一站是第 4 卷:计算机网络

参考

Built with VitePress | Software Systems Atlas