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 项相同工作,事件范围和运行条件一致。
| 指标 | A | B | 解释 |
| 墙钟耗时 | 0.50 s | 0.40 s | B 耗时减少 20% |
| instructions | 2.0 × 10⁹ | 1.2 × 10⁹ | 每项指令从 200,000 降到 120,000 |
| cycles | 1.0 × 10⁹ | 0.8 × 10⁹ | 用户态事件计数下降 |
| IPC | 2.0 | 1.5 | B 的 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 时不能除零或解释为零事件,应报告没有有效计数。
N=Nrawtrunningtenabled,trunning>0
正在呈现知识画面
按时间放大,为什么仍可能估错?按 enabled/running 缩放只修正覆盖比例,不能重建未观察阶段的事件率。窗口恰好覆盖高峰或低谷时,估计可能偏离总量。
{"id":"perf-multiplex-estimate","title":"按时间放大,为什么仍可能估错?","summary":"按 enabled/running 缩放只修正覆盖比例,不能重建未观察阶段的事件率。窗口恰好覆盖高峰或低谷时,估计可能偏离总量。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"start\" data-i18n=\"start\"></label><output for=\"start\" id=\"start-value\"></output><input id=\"start\" type=\"range\" min=\"0\" max=\"9\" value=\"0\" step=\"1\"></div><div class=\"control\"><label for=\"length\" data-i18n=\"length\"></label><output for=\"length\" id=\"length-value\"></output><input id=\"length\" type=\"range\" min=\"0\" max=\"10\" value=\"2\" step=\"1\"></div><div id=\"chart\" class=\"hist\" role=\"img\" data-i18n-label=\"chart\"></div><div class=\"metrics\"><div><span data-i18n=\"raw\"></span><strong id=\"raw\"></strong></div><div><span data-i18n=\"estimate\"></span><strong id=\"estimate\"></strong></div><div><span data-i18n=\"truth\"></span><strong id=\"truth\"></strong></div><div><span data-i18n=\"coverage\"></span><strong id=\"coverage\"></strong></div></div><p class=\"formula\" id=\"equation\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.hist{height:165px;display:grid;grid-template-columns:repeat(10,minmax(0,1fr));gap:4px;align-items:end;padding-bottom:25px;margin-top:15px}.hist div{position:relative;background:var(--surface);border:1px solid var(--rule);min-height:1px}.hist div.on{background:var(--accent-soft);border-color:var(--accent)}.hist small{position:absolute;top:100%;width:100%;text-align:center;font-size:10px}.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const start=+$('#start').value,n=Math.min(+$('#length').value,10-start),rates=Array.from({length:10},(_,i)=>i<2?100:10),raw=rates.slice(start,start+n).reduce((a,b)=>a+b,0);set('start-value',start);$('#length').max=10-start;$('#length').value=n;set('length-value',n);$('#chart').innerHTML=rates.map((r,i)=>'<div class=\"'+(i>=start&&i<start+n?'on':'')+'\" style=\"height:'+r+'%\"><small>'+i+'</small></div>').join('');set('raw',raw);set('estimate',n?(raw*10/n).toFixed(1):'—');set('truth',280);set('coverage',10*n+'%');set('equation',n?'N̂ = '+raw+' × 10 / '+n+' = '+(raw*10/n).toFixed(1):'time_running = 0; N̂ = ∅');$('#estimate').dataset.value=n?raw*10/n:'';}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"chart":"各秒事件数,选中列为实际观测","coverage":"时间覆盖率","estimate":"缩放估计","heading":"按时间放大,为什么仍可能估错?","intro":"移动计数器实际运行窗口,对比缩放估计与模型已知总量。","length":"实际计数时长(s)","raw":"原始观测事件","reset":"重置","scope":"10个1秒区间,前2秒每秒100事件,后8秒每秒10事件;完整总量为280。人工数据用于说明估计偏差,不是 PMU 实测。","scopeLabel":"模型范围与约定","start":"窗口起点(s)","truth":"模型真实总量"}}
采样:定位事件发生时的执行位置
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。内联、尾调用和编译优化会影响源码对应关系;链接与加载 解释了这些产物的关系。
火焰图:聚合调用栈
标准聚合火焰图的左右位置没有时间先后含义,宽度表示归入该调用栈的样本量或权重。颜色通常只是区分框,并不天然表示温度或耗时等级。沿宽框向上看能找到深层热点,但热点也可能是上层反复调用导致。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 只落到采样叶端。横轴不是时间。
{"id":"flamegraph-inclusive-self","title":"父框很宽,父函数自己就很慢吗?","summary":"火焰图宽度表示样本权重,父框包含子调用。相同样本可归入多个祖先,但 self 只落到采样叶端。横轴不是时间。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"parse\" data-i18n=\"parse\"></label><output for=\"parse\" id=\"parse-value\"></output><input id=\"parse\" type=\"range\" min=\"0\" max=\"100\" value=\"60\" step=\"1\"></div><div class=\"control\"><label for=\"send\" data-i18n=\"send\"></label><output for=\"send\" id=\"send-value\"></output><input id=\"send\" type=\"range\" min=\"0\" max=\"100\" value=\"30\" step=\"1\"></div><div class=\"control\"><label for=\"idle\" data-i18n=\"idle\"></label><output for=\"idle\" id=\"idle-value\"></output><input id=\"idle\" type=\"range\" min=\"0\" max=\"100\" value=\"10\" step=\"1\"></div><div class=\"flame\" id=\"flame\"><div class=\"flame-row\"><button data-fn=\"parse\">parse</button><button data-fn=\"send\">send</button><i id=\"blank\"></i></div><div class=\"flame-row\"><button data-fn=\"handle\">handle</button><button data-fn=\"idle\">idle</button></div><div class=\"flame-row\"><button data-fn=\"main\">main</button></div></div><div class=\"metrics\"><div><span data-i18n=\"selected\"></span><strong id=\"selected\"></strong></div><div><span data-i18n=\"self\"></span><strong id=\"self\"></strong></div><div><span data-i18n=\"inclusive\"></span><strong id=\"inclusive\"></strong></div><div><span data-i18n=\"total\"></span><strong id=\"total\"></strong></div></div><p class=\"formula\" id=\"equation\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.flame{margin:18px 0}.flame-row{display:flex;height:46px;gap:0}.flame-row button{padding:0;border-radius:3px;min-width:0;flex-shrink:0;overflow:hidden;font:12px var(--viz-font-ui);background:var(--accent-soft);border:1px solid var(--paper)}.flame-row button[aria-pressed=true]{background:var(--second-soft);outline:2px solid var(--second);outline-offset:-2px}.flame-row i{display:block}.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let selected='handle';function draw(){const a=+$('#parse').value,b=+$('#send').value,c=+$('#idle').value,total=a+b+c,w={parse:a,send:b,idle:c,handle:a+b,main:total},own={parse:a,send:b,idle:c,handle:0,main:0};for(const [id,v]of[['parse',a],['send',b],['idle',c]])set(id+'-value',v);document.querySelectorAll('[data-fn]').forEach(e=>{const n=e.dataset.fn;e.style.width=total?100*w[n]/total+'%':n==='main'?'100%':'0%';e.textContent=total&&w[n]/total<.09?'':n;e.setAttribute('aria-label',n+' · '+w[n]);e.setAttribute('aria-pressed',String(n===selected));e.disabled=!w[n];e.style.visibility=w[n]?'visible':'hidden';});$('#blank').style.width=(total?c/total*100:100)+'%';set('selected',selected);set('self',own[selected]);set('inclusive',w[selected]);set('total',total);set('equation',total?'self = '+own[selected]+'/'+total+'; inclusive = '+w[selected]+'/'+total:t('empty'));$('#flame').dataset.weights=JSON.stringify(w);}document.querySelectorAll('input').forEach(e=>e.oninput=draw);document.querySelectorAll('[data-fn]').forEach(e=>e.onclick=()=>{selected=e.dataset.fn;draw()});draw();","audio":false,"strings":{"empty":"没有样本,无法计算比例","heading":"父框很宽,父函数自己就很慢吗?","idle":"idle 样本权重","inclusive":"包含子调用的权重","intro":"改变三条样本栈的权重,点击任意框查看 self 与累计归因。","parse":"parse 样本权重","reset":"重置","scope":"三个完整栈:main→handle→parse、main→handle→send、main→idle。非负整数权重,画面是聚合示意而非真实 perf.data。","scopeLabel":"模型范围与约定","selected":"选中函数","self":"self 权重","send":"send 样本权重","total":"总样本权重"}}
精确采样:减少位置偏移
普通采样从事件溢出到记录指令位置之间可能存在 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 样本按时间有代表性,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 指标用于解释机制。