1. 基于 perf 的串行热点函数定位与火焰图解读
任务对象与基线程序
本例使用一个刻意保留低效写法的串行程序作为采样对象:它在一个大数组上做多轮“平方根 + 累加”的变换,并混入一个开销较小的辅助函数,便于观察热点分布。程序保存为 hotspot_demo.c,编译时保留符号与调试信息,否则 perf 只能看到地址而无法归因到函数名。
/* hotspot_demo.c —— 教学用串行程序,故意保留低效写法 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define N (1 << 22) /* 约 419 万个 double,约 32 MB */
#define ROUNDS 40
static double buf[N];
/* 热点候选:逐元素 sqrt + 累加,含一次除法 */
double transform_accumulate(double *a, int n, int rounds) {
double acc = 0.0;
for (int r = 0; r < rounds; r++) {
for (int i = 0; i < n; i++) {
a[i] = sqrt(a[i] + 1.0) / 1.0000001;
acc += a[i];
}
}
return acc;
}
/* 辅助函数:开销较小,用于对比归因 */
double checksum(const double *a, int n) {
double s = 0.0;
for (int i = 0; i < n; i++) s += a[i] * 0.5;
return s;
}
int main(void) {
for (int i = 0; i < N; i++) buf[i] = (double)(i % 1000) + 1.0;
double t = transform_accumulate(buf, N, ROUNDS);
double c = checksum(buf, N);
printf("acc=%.6f checksum=%.6f\n", t, c);
return 0;
}
编译命令(-g 保留调试信息,-O2 是常见基线优化级别;若想观察更“原始”的热点,可改用 -O0 对比):
gcc -O2 -g -fno-omit-frame-pointer hotspot_demo.c -o hotspot_demo -lm
-fno-omit-frame-pointer 让调用栈可被可靠回溯,是获得准确火焰图调用链的关键编译选项;缺少它时 perf 常出现栈断裂,火焰图会退化成扁平的“单层”图。采样、归因与火焰图生成链路
perf 的工作方式是周期性中断 CPU 并记录当前指令指针与调用栈,因此采样频率、栈回溯能力和符号解析共同决定归因质量。下图给出从程序运行到火焰图判读的完整数据链路,以及每一步的关键参数与失败表现。
执行采样并读取归因数据
先跑一次采样,频率取 999 Hz(约每毫秒一次),持续到程序自然结束。随后用 perf report 的文本模式查看按符号聚合的开销,这是火焰图之前最直接的定量依据。
# 1) 采样(-g 记录调用栈,-F 999 采样频率)
perf record -F 999 -g -- ./hotspot_demo
# 2) 文本归因:按 Overhead 降序查看
perf report --stdio --sort overhead,symbol | head -n 30
示例输出(教学示例数据,实际数值随机器与编译选项变化):
# Overhead Symbol
# ........ ......................................
61.20% [.] transform_accumulate
18.40% [.] __ieee754_sqrt
9.10% [.] checksum
6.30% [k] copy_user_enhanced_fast_string
2.10% [.] main
1.50% [.] _int_malloc
...
这里 transform_accumulate 自身占 61.2%,其内部调用的 __ieee754_sqrt 另占 18.4%,两者合计约 79.6%,构成第一热点;checksum 仅 9.1%,属于次要开销。注意 [.] 表示用户态符号,[k] 表示内核态符号,后者常来自系统调用或内存拷贝,不应误判为业务热点。
生成火焰图并完成热点归因说明
火焰图由折叠栈文本渲染而来,需要 perf script 输出原始样本、stackcollapse-perf.pl 折叠调用链、flamegraph.pl 生成 SVG。三个脚本来自 Brendan Gregg 的 FlameGraph 工具集,需提前下载到本地。
# 3) 导出样本并折叠调用栈
perf script > perf.out
./stackcollapse-perf.pl perf.out > perf.folded
# 4) 渲染火焰图
./flamegraph.pl perf.folded > hotspot.svg
# 5) 快速核对折叠结果中占比最高的叶子
awk '{n=$NF; $NF=""; print n"\t"$0}' perf.folded \
| sort -rn | head -n 10
第 5 步的 awk 把每行末尾的样本计数提到行首再排序,可直接得到“最热的调用链”。示例结果(教学示例):
48213 hotspot_demo;main;transform_accumulate;__ieee754_sqrt
30102 hotspot_demo;main;transform_accumulate
7415 hotspot_demo;main;checksum
5102 hotspot_demo;main;transform_accumulate;__memmove_avx_unaligned
1690 hotspot_demo;main
据此写出热点归因说明,要求包含:热点函数、占比、调用链位置、开销性质判断、以及后续优化方向。示例归因说明如下:
热点归因说明(基线)
- 第一热点:
transform_accumulate,自身样本约 61.2%,加上其子调用__ieee754_sqrt的 18.4%,合计约 79.6%。 - 调用链位置:
main → transform_accumulate → __ieee754_sqrt,热点叶子集中在浮点开方与除法路径。 - 开销性质:每轮对 419 万个元素做
sqrt与除法,属计算密集型;同时数组读写带来持续访存压力,火焰图中“数组读写”块宽度可观。 - 次要开销:
checksum约 9.1%,为单次遍历,优先级低。 - 优化方向:减少
sqrt/除法调用次数(如代数化简、向量化)、改善数组访问局部性;优化后以本例采样数据为基线对比。
结果校验与常见偏差
归因结论必须可复核,否则后续优化会建立在错误基线上。建议按以下条件逐项确认:
| 检查项 | 通过条件 | 不通过时的处理 |
|---|---|---|
| 符号解析 | perf report 中热点显示为函数名,而非地址或 unknown | 确认编译带 -g,并检查 perf report 是否加载了对应二进制 |
| 栈完整性 | 火焰图中热点上方存在完整调用链,而非孤立单层块 | 加 -fno-omit-frame-pointer 重新编译并重采样 |
| 样本量 | 总样本数足够(通常数千以上),占比稳定 | 提高 -F 或增加 ROUNDS 延长运行时间 |
| 占比一致性 | perf report 与折叠栈排序结果的热点排序一致 | 检查是否混入内核态样本或采样期间有其他负载干扰 |
| 基线可复现 | 重复采样两次,第一热点与占比波动在合理范围 | 固定 CPU 频率、关闭后台任务后重测 |
完成上述步骤后,即得到一份可复用的性能基线:热点函数、占比、调用链与开销性质均已量化。后续任何优化版本都应在相同编译选项、相同输入规模、相同采样频率下重测,并与本基线逐项对比,才能判断优化是否真正生效。