perf 深度使用

本页目录

perf 是 Linux perf_events 的用户态工具,既能统计事件总量,也能在事件发生时采样调用栈。先明确要回答的问题:程序消耗了多少 CPU、热点在哪、某条指令为什么慢,还是请求在等待什么。不同问题需要不同事件和观察范围。

以下命令以 Linux 用户态程序 ./prog 为例。事件支持依赖 CPU、内核、虚拟化环境和 perf 版本;先用 perf list 查看实际可用项。权限也取决于 perf_event_paranoid、capability 和系统策略,不是所有操作都必须 root。

计数:先建立可重复基线

perf stat -r 5 -e task-clock,context-switches,page-faults -- ./prog
perf stat -r 5 -e '{cycles:u,instructions:u}' -- ./prog
perf list

第一条从 CPU 时间与调度、缺页开始;第二条把用户态 cycles 和 instructions 放进同组,方便比较相同测量窗口。-r 5 会实际运行程序五次,输入、数据状态和预热策略要一致;若程序会修改数据,先准备可重复的测试场景。

指标可回答的问题不能直接推出什么
task-clock任务累计使用多少 CPU 时间不等于墙钟时间,多线程累计可更大
instructions / cycles这段负载的 IPC不能跨 CPU 与负载用统一阈值判好坏
branch-misses / branches被计数分支的失误比例单看失误率不能确定总耗时贡献
page-faults发生多少缺页事件不全是磁盘 I/O,包含轻微缺页
context-switches调度切换频度切换多不必然是首要瓶颈

例如重写算法后指令数下降一半,IPC 也下降,但总运行时间仍可能更短。IPC 应结合运行时间和完成工作量解释。cycles:u 的 u 是仅用户态,不是消除频率变化;CPU 频率、SMT、迁核、混合核心与后台干扰仍影响比较。

通用 cache-misses 的具体映射由 PMU 决定,不应默认当作 L1 失效率。需要讨论某层缓存时,选择该 CPU 文档定义的事件并核对分母。

同样完成一万项工作,IPC 降低也可能更快

下面是假设数据,用于练习解释计数,不是某台机器的测量结果。A 与 B 都完成 10,000 项相同工作,事件范围和运行条件一致。

指标AB解释
墙钟耗时0.50 s0.40 sB 耗时减少 20%
instructions2.0 × 10⁹1.2 × 10⁹每项指令从 200,000 降到 120,000
cycles1.0 × 10⁹0.8 × 10⁹用户态事件计数下降
IPC2.01.5B 的 IPC 更低,但做的总工作更省

这时正确结论是 B 完成同样工作更快,不能因 IPC 从 2 降到 1.5 就判回退。若 B 只处理了 8,000 项、跳过了校验或吞掉错误,耗时减少就不能直接作为优化收益。

from math import isclose
jobs = 10_000
a = {"seconds": 0.5, "instructions": 2_000_000_000, "cycles": 1_000_000_000}
b = {"seconds": 0.4, "instructions": 1_200_000_000, "cycles": 800_000_000}
assert a["instructions"] / a["cycles"] == 2
assert b["instructions"] / b["cycles"] == 1.5
assert a["instructions"] / jobs == 200_000
assert b["instructions"] / jobs == 120_000
assert isclose(1 - b["seconds"] / a["seconds"], 0.2)
assert isclose(jobs / b["seconds"] / (jobs / a["seconds"]), 1.25)

耗时减少 20% 对应吞吐提高 25%,因为两者分母不同。task-clock 也要用正确分母:若两秒墙钟内统计到 3.2 秒累计 CPU 时间,平均约使用 1.6 个 CPU 的执行时间,而非“CPU 利用率超过 100% 所以数据坏了”。这个平均值仍不能表示某个时刻的峰值或每个核心的均匀程度。

计数器复用:数字可能经过缩放

硬件同时可用的计数器有限,事件多于可用资源时可能轮流安排。perf 可以按实际运行时间缩放结果,但若负载阶段性很强,短暂观测某一阶段再外推全程会产生误差。

关注输出中的运行比例、not counted 或 not supported。减少事件数、把相关比率的事件分组、重复实验,有助于判断结果是否稳定;分组也可能因为资源约束而无法整体调度。perf stat 的缩放和事件组语义见 手册。

缩放修正覆盖时间,不能恢复没观察到的阶段

假设事件启用 10 秒,实际被硬件计数器调度 2.5 秒,原始计数为 200。按时间比例缩放得到 200 × 10 / 2.5 = 800。这隐含采到时段能代表未采时段的假设;若恰好只覆盖初始化高峰,而后面大量时间处于另一工作阶段,800 可能严重偏离真实总量。

相关比率最好来自同一事件组和兼容的范围,但事件组不是免费保证:硬件资源不足时整个组也可能无法运行。遇到 not supported 先检查事件与 PMU,遇到 not counted 检查调度资源和实际运行,不能把这些状态当成数值零参与除法。perf stat 缩放与分组

按时间放大,为什么仍可能估错?

完整柱形是模型提供的已知事件率,真实测量通常看不到未采区间。默认只计数前2秒,原始200被放大成1000,远高于真实280;移动到后面的低率区间,又可能低估。running=0 时不能除零或解释为零事件,应报告没有有效计数。

正在呈现知识画面
按时间放大,为什么仍可能估错?

按 enabled/running 缩放只修正覆盖比例,不能重建未观察阶段的事件率。窗口恰好覆盖高峰或低谷时,估计可能偏离总量。

采样:定位事件发生时的执行位置

perf record -o perf.data -F 99 -e cycles:u --call-graph dwarf -- ./prog
perf report -i perf.data --stdio
perf annotate -i perf.data --symbol=my_function

这组命令选择用户态周期事件,以目标频率 99 Hz 采样并采集用户栈信息。实际采样频率可能被内核节流或受工作负载影响;不要依赖某个版本的默认事件或默认频率。

-F 指目标采样频率,-c 指事件采样周期,二者含义不同。cycles 样本回答 CPU 活跃时事件的分布;阻塞期间线程没有运行,普通 on-CPU 栈不会直接告诉你完整等待时间。要解释锁等待或 I/O 延迟,需结合调度事件或入口、返回追踪。

调用栈方式也有取舍:frame pointer 方式通常要求相关代码保留帧指针;DWARF 方式记录用户栈供展开,需相应展开信息,采样栈大小影响数据量与截断;LBR 依赖硬件支持。-g 不是保证任何二进制都能得到完整调用链的开关。perf record

符号名、源码行和调用链是不同信息。保留匹配构建的二进制、build-id 和调试符号,避免拿另一次编译的文件解释旧 perf.data。内联、尾调用和编译优化会影响源码对应关系;链接与加载 解释了这些产物的关系。

火焰图:聚合调用栈

CPU 火焰图示意:宽度是样本权重,不是时间轴 main:100% handle_request:60% background:40% parse:40% store:20% 横向排列用于聚合相似栈;纵向向上表示被调用者 父框包含子调用的样本;最顶层宽框是定位候选,优化后仍需测量端到端收益。

标准聚合火焰图的左右位置没有时间先后含义,宽度表示归入该调用栈的样本量或权重。颜色通常只是区分框,并不天然表示温度或耗时等级。沿宽框向上看能找到深层热点,但热点也可能是上层反复调用导致。Brendan Gregg:Flame Graphs

已有 FlameGraph 脚本在 PATH 时:

perf script -i perf.data > out.perf
stackcollapse-perf.pl out.perf > out.folded
flamegraph.pl out.folded > flamegraph.svg

这些脚本属于 FlameGraph 项目,不是 perf 自带子命令。采集丢失样本、符号缺失或栈展开断裂时,应先修复输入质量,不能只看生成的图是否漂亮。计数样本比例与按事件周期加权的占比也要按报告设置区分。

self 与 children 是两种归因,不要重复相加

一个样本栈是 main → handle_request → parse。按 self 归因,它落在 parse 的当前执行位置;按 children 或调用链累计归因,handle_request 和 main 也会包含这个样本。于是父子行的百分比相加可能超过 100%,并不表示 perf 计算错了。

可以分别查看启用和关闭 children 累计后的报告,并记录使用的百分比口径。很宽的父框可能几乎不执行自身代码,只是所有热点的共同调用者;这时在父函数入口改一行并不会自然消除子调用成本。perf report 归因

采样过少时,先增加有效观察量

99 Hz 下,一个仅运行约 20 ms 的单线程 CPU 密集阶段,理想量级也只有约两次采样,实际还受事件与调度影响。此时“某函数占 50%”可能只对应一个样本,不值得据此精确排名。

可让同一工作负载在一次进程内重复足够次数,分别处理初始化与稳态;不要为了增加样本而把输入规模改成完全不同的算法路径。增加频率也有成本,可能扩大采样数据、丢失事件或改变被观察程序。先核对有效样本数、丢失提示与调用栈质量,再考虑提高频率。

父框很宽,父函数自己就很慢吗?

默认 handle 宽度占90%,但 self 为0,因为这些样本的叶端都在 parse 或 send。main 的100%、handle 的90%与 parse 的60%包含重复归因,不能相加当作总耗时。缩窄某个子框时,父框随样本权重一起变化,横向排列不表示函数先后执行。

正在呈现知识画面
父框很宽,父函数自己就很慢吗?

火焰图宽度表示样本权重,父框包含子调用。相同样本可归入多个祖先,但 self 只落到采样叶端。横轴不是时间。

精确采样:减少位置偏移

普通采样从事件溢出到记录指令位置之间可能存在 skid,热点标注的指令未必就是触发事件的指令。精确采样能力与特定 PMU、事件绑定;Intel PEBS 与 AMD IBS 是不同硬件机制。

perf 的 p 修饰符表示对采样 IP 精度的要求,pp 请求零 skid,ppp 的严格支持由事件和硬件决定。不能把 cycles:ppp 写成适用所有机器的“最高精度模式”。先查询支持的精确事件,再据其事件定义解释结果。perf list 的事件修饰符

精确 IP 也不意味着已经证明该指令是根因:缓存层级、数据来源、依赖链或上游控制流仍需进一步观测。Top-down 指标能按微架构分类执行槽位等资源,但支持范围和指标定义应按本机 PMU 确认,不能固定写成“某一代以后都一样”。

动态探测与观察范围

perf probe -x ./prog --add 'work=my_function'
perf probe --list

工具会报告实际事件名称;再把那个名称交给 perf record -e。命令解析符号需要目标确有可探测的位置,源代码行和局部变量解析还可能需要 DWARF。内联、优化掉的变量或一个符号对应多个位置,都不能简化为“任意函数、任意行都能探测”。

记录参数必须核对目标函数 ABI 与探测位置,不能把 x86-64 的寄存器示例直接用于 arm64。结束时按实际事件全名删除本次创建的 probe,而不是清空共享追踪设施。perf probe 手册

附加到进程可使用 -p PID,线程可使用 -t TID。系统范围的短采集可以用 perf record -a ... -- sleep 10,但会观察其他工作负载;选 CPU、进程、用户态或内核态时,要让结论与实际范围一致。

CPU 热点的占比,不能直接当成请求延迟的占比 假设单请求串行执行:CPU 20 ms + 等待 100 ms CPU 20 等待 100 ms parse 占 CPU 40%:8 ms;优化为 4 ms,节省 4 ms CPU 16 等待仍为 100 ms 总延迟 120 → 116 ms,约降 3.3%;仅适用于图示无重叠且其他因素不变的假设。

用请求时间账约束预期收益

假设 CPU 样本按时间有代表性,parse 占 20 ms CPU 工作的 40%,就是 8 ms;把它加速两倍只能省 4 ms。若请求还有 100 ms 串行等待,整体从 120 ms 降到 116 ms,而不是“热点加速两倍,接口也快一倍”。

from math import isclose
cpu_ms, wait_ms = 20, 100
hot_ms = cpu_ms * 0.4
before = cpu_ms + wait_ms
after = before - hot_ms + hot_ms / 2
assert (before, after) == (120, 116)
assert isclose(1 - after / before, 1 / 30)

多线程流水线、异步重叠和排队系统不能简单把各线程 CPU 时间都加到单请求延迟上。高负载下减少 CPU 服务时间还可能减少排队,低负载下则可能几乎看不到延迟变化。应同时记录负载水平、完成吞吐和延迟分布,避免把单请求模型外推到所有场景。

形成可检验的优化结论

先固定输入和完成工作量,再计数,随后采样定位候选;提出一个可解释的机制并修改,最后回到同一端到端指标验证。Amdahl 定律提醒我们:占总耗时 10% 的部分即使完全消除,也只能把原耗时减少 10%。

若热点比例下降但请求时间没变,可能瓶颈转移、负载改变,或原指标只覆盖 CPU 的一部分。保留命令、机器信息、符号版本与基线,才能让“看起来更快”成为可复查的结论。

关键词:perf, perf_events, PMU, IPC, multiplexing, call graph, flame graph, PEBS, IBS, skid, sampling

给每次结论保留最小证据包

保留源码或构建标识、二进制 build-id、完整命令、事件修饰符、输入规模、完成数量、运行次数和测量环境。对于采样,另记录调用栈方式、数据文件及丢失/节流情况;对于计数,另记录事件运行比例与不支持项。

若新版本只比基线快 1%,而重复运行的波动比这更大,先改善实验或增加有代表性的重复,不急着把小差异归因到改动。可以交替执行 A/B 减少随时间漂移的干扰,并把冷启动和稳态分开报告。最终结论应落到“在给定负载下减少了多少时间或资源”,perf 指标用于解释机制。