想象你是城市交通局的观察员,坐在直升机上俯瞰整座城市。你没法同时盯着每条街,于是每隔几秒随机按下快门,拍一张俯瞰照片。一天下来,你把几百张照片冲洗出来,统计哪些路口、哪些写字楼出镜率最高——出镜最多的地方,就是城市最拥堵的地方。 perf 干的是同一件事,只不过它拍的不是街道,而是 CPU 此刻正在执行的函数。它每隔一小段时间就打断程序一次,看一眼程序“堵在”哪个函数里,记录下来。跑完几千次快门,哪个函数上镜最多,哪个函数就是程序的性能瓶颈。这篇文章,带你学会用 perf 这位“空中观察员”做采样、看硬件计数器、画火焰图,并验证优化前后效果。
学会 perf,等于给程序装上了城市交通监控系统——线上 CPU 飙高、接口变慢、缓存疯狂 miss,你都能在几分钟内锁定真凶。这是性能调优工程师的看家本领。
目录
可视化演示
展示perf性能分析流程:火焰图(main→foo→sort/cmp)展示调用栈热点,Stat→Rec→Rpt→Flame四步分析管线定位CPU瓶颈。
本节配有交互动效(进入视口后自动加载,约 3KB)。
perf性能分析核心 火焰图展示调用栈热点:main→foo→sort(30%)/cmp(20%),alloc(15%)→mem(12%)/str(10%),框宽=CPU占比 perf四步:Stat(计数器)→Rec(采样)→Rpt(报告)→Flame(火焰图),硬件计数器IPC/Cache/Br/Cyc定位瓶颈 Sampling: perf record -F 99 -g -- ./app, Flame: perf script | FlameGraph
采样原理:直升机上的快门
城市观察员坐直升机在城市上空盘旋,不盯着每条街看,而是随手按快门。照片里出现最多的路口,就是最繁忙的路口——真实路况不用人工去数,靠“拍”就能还原。
程序运行也像城市车流:CPU 不停地在各个函数之间切换。我们没法无时无刻记录“车在哪儿”,于是每隔固定时间打断程序一下,看一眼此刻正在执行哪个函数。这个动作就叫采样。perf 默认用 CPU 时钟事件驱动采样,把中断瞬间的函数与调用栈记进 perf.data。
每秒钟按多少次快门,叫采样频率 f;程序总共跑了多久,叫时长 T;两者相乘得到总样本数 N。最后统计函数 i 被拍到的次数 n_i,它的占比就近似等于该函数消耗 CPU 的比例——这就是“用统计量还原真相”。
回到直升机:只要快门够密、飞行时间够长,统计出的“繁忙路口榜”就非常接近真实拥堵。perf 的热点排行榜,本质上就是一张城市出镜率榜单。
- 样本总数 N整轮采样收集到的“照片”张数,也就是 perf.data 里样本条目的总数。
- 采样频率 f每秒按下快门的次数,单位 Hz,perf record 中用 -F 指定。
- 运行时间 T程序从开始到退出经历的总时间,单位秒。
- 函数 i 的占比 P_i照片里拍到函数 i 在执行的次数 n_i 除以总样本数,再乘以 100%。
命令总览:观察员的四件套
观察员不会只有一个动作。他带望远镜、计数器、录像机和回放设备,perf 也把工具分成四件套。
perf top 像直升机上的直播大屏,实时刷新当前 CPU 最忙的进程和函数,适合“先扫一眼”;perf stat 像跑完一圈后的总结报告,给出 cache miss、分支预测失败、IPC 等汇总数字;perf record 像录像机,把采样数据录进 perf.data;perf report 像回放设备,打开录像带生成带调用栈的图文报告。
record 与 report 是黄金组合。record 每次中断除了记录当前函数,还会抓取整条调用栈,方便还原“谁调用了谁”。一份带调用栈的样本要占几百字节,样本越多,perf.data 越大。
所以聪明的观察员不会一上来就录像:先用直播大屏 perf top 扫一眼,再用 perf stat 摸底,最后才决定要不要 record 和 report。四件套配合,层层深入。
- 数据体积 Data生成的 perf.data 文件大小,单位可以是 MB 或 GB。
- 样本总数 N采样次数,等于采样频率乘以运行时长。
- 调用栈深度 Depth每次样本记录的栈帧层数,层数越深,单条样本越大。
采样频率:快门按多快
拍飞驰的赛车,快门太慢会糊成一团;快门太快,存储卡几秒就爆。快门速度,就是采样频率。
采样有个直觉法则:采样频率必须远高于目标事件的频率。程序里短促的函数像一闪而过的赛车,如果频率太低,一个只运行 0.1 毫秒的短函数可能一次都进不了照片,热点被整个漏掉。反之,频率过高时,每次采样都要打断程序、抓调用栈、写文件,这些开销会拖慢程序本身,甚至改变程序的真实行为。
实践中的经验是:先用 -F 99 粗测找方向,再用 -F 999 甚至更高精确定位。低频防失真,高频抠细节,中间的多试几次。
好摄影师会先试拍几张看曝光。我们同样建议:先小频率粗测,再加大频率精确定位,别一上来就 10000 Hz——小心把相机(程序)震坏。
- 采样频率 f_采样perf record -F 指定的每秒采样次数,比如 -F 999 就是每秒 999 次。
- 关键事件频率 f_事件短促函数或高频调用出现的频率,采样频率必须远大于它才捕捉得到。
- 采样间隔 Δt两次快门之间的时间,等于频率的倒数。1/1000 秒就是 1 毫秒一次。
硬件计数器:看见看不见的堵点
直升机照片只能说明“哪里堵”,但一座桥堵,可能是桥面窄(代码逻辑重),也可能是收费站太慢(等内存)。perf 还有一组隐形传感器——硬件性能计数器,由 CPU 内部直接提供。
三个常见指标。cache-misses:数据在最后一级缓存没命中,不得不去内存取,一次 miss 要等上百个周期;branch-misses:分支预测猜错后,流水线要作废重来;IPC:每个时钟周期执行的指令数,IPC 越低说明 CPU 多数时间在“等”(等内存、等分支),而不是在“算”。
perf stat 会直接给出这些汇总。缓存 miss 率等于 miss 次数除以访问总次数。如果 miss 率很高而热点函数又是内存密集操作,就要考虑重构数据结构、提高局部性。
这一组传感器像地铁闸机上的计数器:车(数据)过闸时被挡了一下(miss),计数器咔嗒一声。一天下来,你就知道哪些闸机最堵。硬件计数器让“看不见的等待”变成白纸黑字的数字。
- 未命中次数 C_miss数据请求在缓存里没找到、必须去主存取数的次数。
- 访问总次数 C_accessCPU 对缓存发起的全部访问次数,命中加未命中。
- 完成指令数 I程序实际执行完毕的指令条数。
- 时钟周期 C_cyclesCPU 消耗的周期总数。IPC 越低,说明“等得多、算得少”。
火焰图生成:把照片拼成热力地图
光有“哪条街最堵”还不够,市长还想知道“堵是从哪个区导流过来的”。火焰图就是这个叙事视图:把每条调用栈叠成一座火焰山,宽度代表停留时间占比,从下往上是根到叶的调用路径。
生成只需三步:先用 perf record -g 采集带调用栈的样本(录像时每帧附上 GPS 轨迹);再用折叠脚本把多行调用栈压成一行,比如 main;parse;compute 42,这一步叫栈折叠;最后用 FlameGraph 工具把折叠后的数据画成 SVG。
解读时记住三条:顶部宽宽的平顶是性能问题的聚集地,说明某叶子函数独占了 CPU;颜色只是随机分配,与好坏无关;尖尖的山峰是频繁进出的小函数,平缓的熔岩层是持续霸占 CPU 的长函数。
有了热力地图,从上往下看:哪座山最宽,就沿着它往山下(上游调用者)走,找到真正值得优化的一整条调用链。这就是“拍照 → 折叠 → 画图 → 读图”的完整闭环。
- 火焰条宽度 W_i函数 i 在火焰图中的横向宽度,宽的就是停留久的。
- 该帧样本数 n_i函数 i 出现在调用栈中的次数。
- 总样本数 N火焰图最底层(通常是 main)的样本总数,对应满宽。
- 总宽度 W_total火焰图的横向全程,代表 100% 的样本。
CPU热点定位:锁定最忙的街区
城市报告说“金融街最忙”,我们还要回答:是修路还是红绿灯配时?热点定位的产出不是一句话,而是一条可执行的结论。
方法是:先用足够的频率 record,再用 perf report 按 self 列排序——self 是函数自己消耗的 CPU,不含子函数;children 才包含子函数。排在最前面的 self 大户就是候选热点。接着展开调用链,看流量来自哪几个 caller,结合源码确认:是调用次数太多,还是单次太慢,还是被锁和 IO 拖住。
一个常见陷阱:某函数很宽,但它是在等 IO 或等锁,优化它本身没用,要改的是上游并发度或锁设计。热点函数的大致耗时可以用总时间乘以样本占比估算。
锁定最忙街区后还要沿街巡查:看入口 caller 能否缓存、能否减少调用次数。定位不是目的,定位之后能行动才是目的。
- 估计耗时 T_i函数 i 大概吃掉了多少秒程序时间。
- 样本占比 n_i/N函数 i 的样本数占全体样本的比例,也是火焰图里的宽度。
- 总运行时间 T_totalperf stat 里的 elapsed time 或 wall time。
对比调优:优化后重新拍一张
修完路,要重新航拍验证效果。所有优化都要用同一把尺子量:perf stat 优化前后各跑一遍,对比 elapsed、IPC、cache-misses 和分支预测失败次数。
动手前先算一笔账——Amdahl 定律。它说整体加速比只取决于被优化部分的占比和加速倍数。如果某函数占 60% 时间,把它优化 2 倍,整体最多加速 1.43 倍;就算优化 100 倍,整体极限也只有 2.5 倍。所以别在 2% 的热点上耗三天,先优化占比最大的那块。
观察员养成的习惯是:每次修改只变一个变量,记录前后 perf stat 的完整数字。缓存 miss 降了但 IPC 没升?想想为什么。持续“拍一张 → 对比 → 再拍”,每次微小的进步汇聚成可观的性能提升。
- 原占比 p被优化函数在优化前占程序总时间的比例,比如 0.6。
- 局部加速比 s该函数优化后快了多少倍,比如从 10 秒优化到 5 秒,s = 2。
- 整体加速比 S整个程序最终的加速倍数。p = 0.6、s = 2 时,S = 1 / (0.4 + 0.3) ≈ 1.43。
AI 应用场景
场景一:大模型推理服务的 CPU 飙高排查
阶段:模型推理与在线服务优化
解决的问题:传统排查靠猜、靠加机器,无法量化“CPU 到底烧在哪”。用 perf 这位空中观察员,先 perf top 看实时热点,再 perf record 抓调用栈画火焰图,把模型前处理、注意力计算、显存拷贝、日志输出逐一量化,定位真凶。
具体案例:某对话机器人服务压测时 p99 延迟暴涨,perf record 的火焰图显示 ext4 文件写入和日志序列化占了大片宽度——瓶颈不在模型,而在逐条同步写日志。改为批量异步落盘后,QPS 提升 2.3 倍。
场景二:向量检索服务的缓存命中率调优
阶段:数据预处理与特征检索
解决的问题:向量召回慢,perf stat 显示 cache-misses 率很高,说明数据访问局部性差,CPU 大量时间在等内存而不是计算。
具体案例:批量 embedding 查询时按用户 id 分散访问,perf stat 显示 miss 率 35%;改为按分区顺序批量读取后,miss 率降到 12%,p99 延迟下降 60%。
Python 演示
import signalimport time # 模拟 perf: 每隔固定间隔打断程序, 看 CPU 此刻在执行哪个函数samples = [] # 每次采样记录当前函数名interval = 0.001 # 采样间隔 1ms, 相当于 -F 1000 def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2) def busy_square(n): total = 0 for i in range(n): total += i * i return total def main(): result = 0 for _ in range(3): result += busy_square(200000) # 快速但琐碎的活 result += fib(28) # 真正的 CPU 大热点 result += busy_square(300000) return result def sampler(signum, frame): # 相当于 perf 的中断处理: 记录正在执行的函数名 samples.append(frame.f_code.co_name) signal.signal(signal.SIGALRM, sampler) def run_profile(): # 设置周期性定时器: 每 1ms 触发一次采样 signal.setitimer(signal.ITIMER_REAL, interval, interval) start = time.time() main() signal.setitimer(signal.ITIMER_REAL, 0) # 跑完关闭采样 elapsed = time.time() - start counts = {} for name in samples: counts[name] = counts.get(name, 0) + 1 # 按函数聚合样本 print("elapsed: %.3fs samples: %d" % (elapsed, len(samples))) for name, num in sorted(counts.items(), key=lambda kv: kv[1], reverse=True): print("%-14s %5d %5.1f%%" % (name, num, num * 100.0 / len(samples))) if __name__ == "__main__": run_profile()
signal.signal(signal.SIGALRM, sampler)注册中断处理器:每次响铃都跳进 sampler。signal.setitimer(signal.ITIMER_REAL, interval, interval)设置周期性定时器,相当于 perf record 的 -F 1000。samples.append(frame.f_code.co_name)中断瞬间记录当前正在执行的函数名,这就是“拍一张照片”。counts[name] = counts.get(name, 0) + 1把所有样本按函数名聚合,相当于 perf report 的统计。num * 100.0 / len(samples)计算每个函数的热点占比,对应文章里的样本占比公式。
常见误区
误区perf 显示函数 A 占了 80%,那 A 一定写得很烂,我把它整个重写就好了。
正解样本只告诉你“CPU 在这里停留”,不告诉你“为什么停留”。A 可能是被上层疯狂调用(调用次数爆炸),也可能是在自旋等待锁或内存。要先看调用链和硬件计数器,找到“流量来源”再动手。就像某路口照片多,可能是所有车都被导航导到这里——拆路口不如改导航。
为何常见:人脑天然把“停留时长”和“代码质量”画等号,忽略了调用次数与等待这两个隐藏变量。
误区我把 -F 设成 99999,样本多到爆炸,结果肯定最准。
正解每次采样都要打断程序、抓栈、写文件。高频采样本身就在拖慢程序,甚至改变函数之间的时间分布;而且数据量巨大,分析起来反而困难。实际中 -F 99 到 -F 999 就足以定位绝大多数热点。
为何常见:觉得“数据越多越客观”,忽略了观测行为本身对被测系统的扰动。
误区火焰图里红色的一定是问题函数,要优先处理它。
正解火焰图的颜色是随机分配的,只为区分相邻帧,与函数性能无关。读图只看宽度,宽度代表样本占比。真正要关注的是宽而平的大块,而不是颜色深浅。
为何常见:人们受红绿灯、热力图色彩语义的影响,不自觉给颜色赋予“危险”或“健康”的含义。
误区CPU 都跑满了,肯定是纯计算问题,加核就能解决。
正解CPU 100% 也可能是在空转:自旋锁、忙等、轮询,或者 cache miss 后不停重试。用 perf stat 看 IPC,如果 IPC 很低,说明 CPU 在“等”而不是在“算”,此时加核无用,要查锁、查内存布局。
为何常见:把“CPU 利用率”和“计算效率”混为一谈,忽略了等待类周期也消耗 CPU。
面试题
Q1. perf 采样的本质是下列哪一种做法?
- A.逐个运行函数并记录各自耗时
- B.高频打断程序记录执行位置← 正确答案
- C.静态扫描源码寻找死循环
- D.在每行代码插入计时桩点
答案:B。perf 的核心是周期性中断程序,记录中断瞬间正在执行的函数,用样本占比近似耗时。A 是插桩计时,C 是静态分析,D 是代码注入,都不是采样。
Q2. 火焰图中,一个函数帧的横向宽度代表什么?
- A.该函数被调用的先后顺序
- B.该函数占全体样本的比例← 正确答案
- C.该函数源码的行数总和
- D.该函数分配的线程数量
答案:B。宽度正比于样本占比,即时间占比。A 的先后顺序由纵向层次体现,C 和 D 与采样无关。火焰图只看宽度,不看颜色。
Q3. 想实时观察当前系统 CPU 热点函数,应使用哪个命令?
- A.perf top 实时榜单← 正确答案
- B.perf build 编译缓存
- C.perf mem 内存剖析
- D.perf probe 动态探针
答案:A。perf top 像直升机直播大屏,实时刷新当前最热函数。perf mem 是内存剖析,perf probe 是动态探针,而 perf build 不是标准子命令。
Q4. perf record -F 1000 的含义是什么?
- A.每秒采集一千次性能样本← 正确答案
- B.总共记录一千个函数调用
- C.每千条指令暂停并采样
- D.采集时长限制一千秒钟
答案:A。-F 指定的是采样频率,即每秒 1000 次,相当于快门每秒按 1000 次。它不限制总数、也不按指令数采样。
Q5. 关于缓存未命中 cache miss 的说法,正确的是?
- A.仅影响程序首次启动阶段
- B.未命中要访内存执行变慢← 正确答案
- C.性能和缓存未命中无关
- D.指针多才会引发未命中
答案:B。cache miss 意味着数据要去更慢的内存取,一次可能损失上百个周期,是常见性能杀手。它贯穿整个运行过程,也与代码数据布局强相关。
Q6. 判断某热点函数真正“值得优化”,需要结合哪些信息?
- A.该函数的样本占比很高← 正确答案
- B.它是自旋等待还是真计算← 正确答案
- C.上层调用者的调用路径← 正确答案
- D.函数名长度与代码风格
答案:ABC。占比高是前提;若它是在等锁或等 IO,优化本身没用,要看调用链找源头。函数名长度和风格与性能无关。
Q7. perf stat 结果中的 IPC 指标指的是什么?
- A.每秒处理的中断请求数量
- B.每个时钟周期执行的指令数← 正确答案
- C.指令缓存未命中的比例值
- D.进程上下文的切换频率值
答案:B。IPC = 指令数 / 时钟周期数。IPC 低说明 CPU 大量时间在等待(内存、分支),而不是在真正计算,是“CPU 在忙还是在空转”的判据。
Q8. 采样频率设置过低可能带来哪些后果?
- A.短小的函数根本采样不到← 正确答案
- B.采样的开销会大幅上升
- C.热点分布估计严重失真← 正确答案
- D.火焰图自动变成彩色图
答案:AC。频率过低时,运行时间短的函数一次都进不了样本,热点占比会严重偏离真实,就像慢快门拍不到飞驰的赛车。频率低反而开销小,火焰图颜色与频率无关。
Q9. 下列哪些数据可以来自 perf stat 的硬件计数器?
- A.缓存未命中的累计次数← 正确答案
- B.分支预测失败的次数← 正确答案
- C.磁盘剩余容量的数值
- D.已退休指令的总数量← 正确答案
答案:ABD。缓存未命中、分支预测失败、已退休指令都是 CPU 硬件计数器。磁盘容量属于存储系统,不是 CPU 性能计数器。
Q10. 若函数 A 占运行时间 60%,优化后快了一倍,整个程序加速多少?
- A.加速 2.0 倍左右
- B.加速 1.43 倍左右← 正确答案
- C.加速 1.25 倍左右
- D.加速 0.80 倍左右
答案:B。由 Amdahl 定律,S = 1 / ((1 - 0.6) + 0.6 / 2) = 1 / 0.7 ≈ 1.43。局部加速 2 倍不等于整体加速 2 倍,这就是“先算账再优化”的原因。