perf 与火焰图:从热点到定位(接 Perfetto)

发布时间:2026/10/19 7:07:45
perf 与火焰图:从热点到定位(接 Perfetto)
「程序慢」是个太笼统的问题。要优化第一步永远是定位热点hotspot找到那 20% 的代码它们占了 80% 的运行时间。Linux 的 perf 就是干这个的它靠采样告诉你「CPU 大部分时间花在哪个函数上」。这篇讲透采样型 profiler 的原理、perf 命令用法、火焰图的读法以及它和 Perfetto 的分工。关于实测两件事说清① perf 二进制在线沙箱里是有的/usr/bin/perf7.0.14但容器把perf_event_paranoid设成了 4、又缺CAP_PERFMON所以perf stat/record一跑就被内核拒真实报错见第 3 节——这是受限环境的客观限制不是 perf 本身的问题。② 于是「定位热点」这一步这篇换用 gprof 实测了gprof 是插桩型 profiler不需要内核采样权限真实调用图见第 2.5 节。perf 命令本身仍是用法示例标注出来是为了不假装跑过。1. 引子热点函数长什么样先看一个典型的「有明显热点」的程序。它大部分时间都耗在一个函数里#includecstdio#includecstdint// 热点函数内部有循环CPU 时间大部分耗在这里std::int64_thot_loop(std::int64_tx){std::int64_tacc0;for(std::int64_ti0;i1000;i){accx;// perf 采样大概率命中这一行}returnacc;// 结果是 x * 1000}intmain(){std::int64_ttotal0;for(std::int64_tn1;n3;n){totalhot_loop(n);// 1*1000 2*1000 3*1000 6000}std::printf(total %lld\n,(longlong)total);return0;}total 6000hot_loop就是「热点」它的执行时间在整个程序里占比最高。找到它优化就有目标了。问题是真实程序几千个函数你怎么知道热点是谁2. 采样型 profiler 的原理perf 不是「计时每个函数」而是周期采样sampling每隔固定时间比如 1 毫秒让 CPU 中断一下记录「此刻正在执行哪个函数」。采样足够多次后统计每个函数被命中的次数命中多的就是热点。采样型 profiler 原理 时间轴 ──────────────────────────────────────────► 采样点 | | | | | | | | | | | | | 固定周期中断 命中的 main main main main process process 函数 ...多数命中热点函数少数命中冷函数 │ ▼ 统计命中次数 → 占比高的函数 热点采样的本质是「用概率逼近时间占比」某函数占 90% 的运行时间那 100 次采样里大约 90 次会命中它。用一个确定性例子感受「命中占比」的含义#includecstdio// 用调用次数模拟采样命中被调用越多的函数越容易被「采到」intcalls_hot0;intcalls_cold0;inthot(){calls_hot;return1;}intcold(){calls_cold;return1;}intmain(){for(inti0;i1000;i){hot();// 热点调 1000 次}cold();// 冷函数只调 1 次std::printf(hot 被调用 %d 次cold 被调用 %d 次\n,calls_hot,calls_cold);inttotalcalls_hotcalls_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。它是插桩型instrumentationprofiler-pg编译时在函数入口埋计数器运行后生成gmon.outgprof再解析出调用图和调用次数。它测的不是「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 / reportperf 最常用的三个子命令命令作用输出perf stat ./prog统计整体指标指令数、缓存未命中、分支预测失败等一行行计数器perf record ./prog采样运行把命中记录写进perf.data生成数据文件perf report读perf.data按函数汇总命中占比热点排行榜用法示例。但先说清这些命令在受限容器里会怎样perf 二进制在/usr/bin/perf7.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_paranoid4 缺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里的-gcall-graph很关键只有记录了调用栈才能画火焰图。没有-g你只知道「热点是某个函数」不知道「是谁调它调出来的」。4. 火焰图怎么读火焰图Flame Graph把采样到的调用栈堆叠起来是一种直观展示热点的方式火焰图示意横轴 采样占比/时间纵轴 调用栈深度 ┌──────────────────────────────┬──────┐ │ compute │ cold │ ← 栈顶叶子函数最底层实现 ├──────────────────────────────┼──────┤ │ process │ │ ├──────────────────────────────┤ │ │ main │ │ ← 栈底程序入口 └──────────────────────────────┴──────┘ ◄────────── 宽 热点 ─────────►◄ 窄 ►读火焰图只有两条规则横轴宽度 该函数占用的时间/采样比例不是函数执行时长一个函数可能因为「被反复调用」而显得很宽。纵轴 调用栈深度从下往上是调用链main→process→compute。最上面一层叫「栈顶」是真正干活的叶子函数。所以火焰图上「最宽的那一块」就是你要优化的目标。如果compute那一块特别宽说明 CPU 时间主要花在compute内部优化它收益最大。官方文档火焰图Brendan Gregg —— 火焰图的发明人写的权威教程含 SVG 交互版5. 与 Perfetto 的分工采样 vs traceperf 定位「哪个函数是热点」但它回答不了「一次请求从 A 服务到 B 服务中间慢在哪一步」。后者是trace 系统的活儿典型代表是 Perfetto维度perfPerfetto定位Linux 系统级采样 profiler端到端 trace 系统原理周期采样 栈回溯事件插桩instrumentation 时间线粒度函数级 CPU 热点跨进程/跨线程的完整事件流回答的问题「CPU 花在哪个函数」「这次请求的延迟耗在哪段」典型场景单机性能优化、找热点函数系统级延迟分析、多组件联动排查简单说perf 看「局部谁最慢」Perfetto 看「全局哪一步慢」。两者是互补关系先用 perf 把单机热点压下去再用 Perfetto 分析跨组件的端到端延迟。官方文档Linux perfperf wiki 与 Perfetto 文档 —— 各自领域的权威手册6. 完整示例一条可被火焰图展开的调用链把「调用链」这个火焰图核心概念用一个完整程序落下来#includecstdio#includecstdint// 三层调用链main - process - compute// 火焰图里这条链会从下往上堆成三块宽度由采样占比决定std::int64_tcompute(std::int64_tx){std::int64_tacc0;for(std::int64_ti0;i1000;i){accx;// 真正干活的叶子层}returnacc/1000;// 结果等于 x但中间有 1000 次循环的工作量}std::int64_tprocess(std::int64_tx){returncompute(x)compute(x1);// 两次调用 compute}intmain(){std::int64_ttotal0;for(std::int64_tn0;n5;n){totalprocess(n);// process(n) n (n1) 2n1}std::printf(total %lld\n,(longlong)total);// 13579 25return0;}total 25官方文档Compiler Explorergodbolt —— 分析热点时先在这里看编译器把热点函数优化成了什么汇编往往比盲改更有效对着这段程序跑perf record -g ./demo perf report你会看到compute是占比最高的叶子函数而它的父级是process、再上级是main。这条「竖柱」正是火焰图要画的东西。优化顺序永远是先看最宽的栈顶叶子函数再决定是优化它本身还是减少调用它的次数。7. 延伸阅读Linux perf — perf wikiperf 子命令、事件、示例的权威索引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 看全局——「先测量再优化」是铁律。