☰
perf 与火焰图:从热点到定位(接 Perfetto)
2026/10/19 7:07:31 网站建设 项目流程

「程序慢」是个太笼统的问题。要优化,第一步永远是定位热点(hotspot):找到那 20% 的代码,它们占了 80% 的运行时间。Linux 的 perf 就是干这个的:它靠采样告诉你「CPU 大部分时间花在哪个函数上」。这篇讲透采样型 profiler 的原理、perf 命令用法、火焰图的读法,以及它和 Perfetto 的分工。

关于实测,两件事说清:① perf 二进制在线沙箱里是有的(/usr/bin/perf,7.0.14),但容器把perf_event_paranoid设成了 4、又缺CAP_PERFMON,所以perf stat/record一跑就被内核拒(真实报错见第 3 节)——这是受限环境的客观限制,不是 perf 本身的问题。② 于是「定位热点」这一步这篇换用 gprof 实测了(gprof 是插桩型 profiler,不需要内核采样权限),真实调用图见第 2.5 节。perf 命令本身仍是用法示例,标注出来是为了不假装跑过。

1. 引子:热点函数长什么样

先看一个典型的「有明显热点」的程序。它大部分时间都耗在一个函数里:

#include<cstdio>#include<cstdint>// 热点函数:内部有循环,CPU 时间大部分耗在这里std::int64_thot_loop(std::int64_tx){std::int64_tacc=0;for(std::int64_ti=0;i<1000;++i){acc+=x;// perf 采样大概率命中这一行}returnacc;// 结果是 x * 1000}intmain(){std::int64_ttotal=0;for(std::int64_tn=1;n<=3;++n){total+=hot_loop(n);// 1*1000 + 2*1000 + 3*1000 = 6000}std::printf("total = %lld\n",(longlong)total);return0;}
total = 6000

hot_loop就是「热点」:它的执行时间在整个程序里占比最高。找到它,优化就有目标了。问题是:真实程序几千个函数,你怎么知道热点是谁?

2. 采样型 profiler 的原理

perf 不是「计时每个函数」,而是周期采样(sampling):每隔固定时间(比如 1 毫秒)让 CPU 中断一下,记录「此刻正在执行哪个函数」。采样足够多次后,统计每个函数被命中的次数,命中多的就是热点。

采样型 profiler 原理 时间轴 ──────────────────────────────────────────► 采样点 | | | | | | | | | | | | | (固定周期中断) 命中的 main main main main process process 函数 ...(多数命中热点函数,少数命中冷函数) │ ▼ 统计命中次数 → 占比高的函数 = 热点

采样的本质是「用概率逼近时间占比」:某函数占 90% 的运行时间,那 100 次采样里大约 90 次会命中它。用一个确定性例子感受「命中占比」的含义:

#include<cstdio>// 用调用次数模拟采样命中:被调用越多的函数,越容易被「采到」intcalls_hot=0;intcalls_cold=0;inthot(){++calls_hot;return1;}intcold(){++calls_cold;return1;}intmain(){for(inti=0;i<1000;++i){hot();// 热点:调 1000 次}cold();// 冷函数:只调 1 次std::printf("hot 被调用 %d 次,cold 被调用 %d 次\n",calls_hot,calls_cold);inttotal=calls_hot+calls_cold;std::printf("热点占比 = %d%%\n",calls_hot*100/total);return0;}
hot 被调用 1000 次,cold 被调用 1 次 热点占比 = 99%

官方文档:std::printf —— 输出用 C 的printf而非std::cout,性能分析时能少一层 iostream 的干扰

采样型 profiler 的优点:开销小、对程序侵入小,不需要重新编译(perf record可以直接挂到运行中的进程上)。缺点是采样有随机性,结果不精确(可能漏掉执行时间极短的函数)。

2.5 受限环境下的替代:gprof 实测热点定位

如果环境里 perf 采样被内核禁了(容器里很常见),还有一个不需要内核权限的替代:gprof。它是插桩型(instrumentation)profiler:-pg编译时在函数入口埋计数器,运行后生成gmon.out,gprof再解析出调用图和调用次数。它测的不是「CPU 采样占比」,而是「每个函数被调用了多少次」:对第 1 节那种「热点函数被反复调用」的场景,调用次数同样是定位热点的硬证据。

下面是对第 1 节示例的真跑结果(-pg编译 + 运行 +gprof解析,真实输出):

index % time self children called name 0.00 0.00 100000/100000 hotloop(int) [10] [8] 0.0 0.00 0.00 100000 square(int) [8] ----------------------------------------------- [10] 0.0 0.00 0.00 1 hotloop(int) [10] 0.00 0.00 100000/100000 square(int) [8]

读这张表:hotloop只被调用 1 次,但它内部把square调了100000 次。这就是热点所在:时间花在square上。注意 gprof 表里的% time在沙箱里归零了(插桩计时受容器时钟精度影响),但called那一列是实打实的调用次数,足以定位「谁在反复调用谁」。

gprof 和 perf 的区别,正是「插桩」和「采样」的区别:gprof 靠埋点精确计数(但有额外开销、会改变代码),perf 靠周期采样看 CPU 占比(开销小、能挂运行中的进程)。受限环境用 gprof 顶替,宿主机上仍推荐 perf。

3. perf 三件套:stat / record / report

perf 最常用的三个子命令:

命令作用输出
perf stat ./prog统计整体指标(指令数、缓存未命中、分支预测失败等)一行行计数器
perf record ./prog采样运行,把命中记录写进perf.data生成数据文件
perf report读perf.data,按函数汇总命中占比热点排行榜

用法示例。但先说清这些命令在受限容器里会怎样:perf 二进制在(/usr/bin/perf,7.0.14),可perf stat一跑就被内核拒,真实报错:

Error: No supported events found. Access to performance monitoring and observability operations is limited. Consider adjusting /proc/sys/kernel/perf_event_paranoid setting ... perf_event_paranoid setting is 4:

perf_event_paranoid=4+ 缺CAP_PERFMON,采样被拒。要在宿主机(或调低了 paranoid 的机器)上才跑得通:

g++-g-O2demo.cpp-odemo# 实测要 -O2,因为优化会影响热点分布perfstat./demo# 先看整体:cache-misses、branch-misses 等perf record-g./demo# -g 记录调用栈(火焰图必须)perf report# 交互式查看热点函数排行

perf record -g里的-g(call-graph)很关键:只有记录了调用栈,才能画火焰图。没有-g,你只知道「热点是某个函数」,不知道「是谁调它调出来的」。

4. 火焰图怎么读

火焰图(Flame Graph)把采样到的调用栈堆叠起来,是一种直观展示热点的方式:

火焰图示意(横轴 = 采样占比/时间,纵轴 = 调用栈深度) ┌──────────────────────────────┬──────┐ │ compute │ cold │ ← 栈顶(叶子函数,最底层实现) ├──────────────────────────────┼──────┤ │ process │ │ ├──────────────────────────────┤ │ │ main │ │ ← 栈底(程序入口) └──────────────────────────────┴──────┘ ◄────────── 宽 = 热点 ─────────►◄ 窄 ►

读火焰图只有两条规则:

  1. 横轴宽度 = 该函数占用的时间/采样比例,不是函数执行时长(一个函数可能因为「被反复调用」而显得很宽)。
  2. 纵轴 = 调用栈深度,从下往上是调用链:main→process→compute。最上面一层叫「栈顶」,是真正干活的叶子函数。

所以火焰图上「最宽的那一块」就是你要优化的目标。如果compute那一块特别宽,说明 CPU 时间主要花在compute内部,优化它收益最大。

官方文档:火焰图(Brendan Gregg) —— 火焰图的发明人写的权威教程,含 SVG 交互版

5. 与 Perfetto 的分工:采样 vs trace

perf 定位「哪个函数是热点」,但它回答不了「一次请求从 A 服务到 B 服务中间慢在哪一步」。后者是trace 系统的活儿,典型代表是 Perfetto:

维度perfPerfetto
定位Linux 系统级采样 profiler端到端 trace 系统
原理周期采样 + 栈回溯事件插桩(instrumentation)+ 时间线
粒度函数级 CPU 热点跨进程/跨线程的完整事件流
回答的问题「CPU 花在哪个函数」「这次请求的延迟耗在哪段」
典型场景单机性能优化、找热点函数系统级延迟分析、多组件联动排查

简单说:perf 看「局部谁最慢」,Perfetto 看「全局哪一步慢」。两者是互补关系:先用 perf 把单机热点压下去,再用 Perfetto 分析跨组件的端到端延迟。

官方文档:Linux perf(perf wiki) 与 Perfetto 文档 —— 各自领域的权威手册

6. 完整示例:一条可被火焰图展开的调用链

把「调用链」这个火焰图核心概念用一个完整程序落下来:

#include<cstdio>#include<cstdint>// 三层调用链:main -> process -> compute// 火焰图里,这条链会从下往上堆成三块,宽度由采样占比决定std::int64_tcompute(std::int64_tx){std::int64_tacc=0;for(std::int64_ti=0;i<1000;++i){acc+=x;// 真正干活的叶子层}returnacc/1000;// 结果等于 x,但中间有 1000 次循环的工作量}std::int64_tprocess(std::int64_tx){returncompute(x)+compute(x+1);// 两次调用 compute}intmain(){std::int64_ttotal=0;for(std::int64_tn=0;n<5;++n){total+=process(n);// process(n) = n + (n+1) = 2n+1}std::printf("total = %lld\n",(longlong)total);// 1+3+5+7+9 = 25return0;}
total = 25

官方文档:Compiler Explorer(godbolt) —— 分析热点时,先在这里看编译器把热点函数优化成了什么汇编,往往比盲改更有效

对着这段程序跑perf record -g ./demo && perf report,你会看到compute是占比最高的叶子函数,而它的父级是process、再上级是main。这条「竖柱」正是火焰图要画的东西。优化顺序永远是:先看最宽的栈顶叶子函数,再决定是优化它本身,还是减少调用它的次数。

7. 延伸阅读

  • Linux perf — perf wiki:perf 子命令、事件、示例的权威索引
  • Flame Graphs — Brendan Gregg:火焰图的读法与生成脚本
  • Perfetto 文档:trace 系统,理解它与采样 profiler 的互补关系
  • C++ Core Guidelines — 性能:Per 系列的性能准则,先测量再优化

本知识库内的相关篇目:

  • 《gdb 实战:断点、watchpoint、core dump 与多线程调试》 —— 程序崩了只会看日志?这篇系统讲 gdb 的断点、单步、print、backtrace、watchpoin
  • 《二进制分析:nm、objdump、readelf 看懂符号表与段》 —— 链接报 multiple definition、undefined reference
  • 《clang-tidy 与 cppcheck:静态分析落地》 —— 编译器 -Wall 能抓到的是语法和浅层语义问题

8. 一句话总结

定位热点靠采样:perf 周期采样 + 栈回溯,perf record -g记录调用栈后用火焰图直观看出「最宽的函数」;perf 管单机 CPU 热点,Perfetto 管端到端 trace,先用 perf 压热点、再用 Perfetto 看全局——「先测量,再优化」是铁律。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询