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 并记录当前指令指针与调用栈,因此采样频率、栈回溯能力和符号解析共同决定归因质量。下图给出从程序运行到火焰图判读的完整数据链路,以及每一步的关键参数与失败表现。

perf 采样 → 折叠栈 → 火焰图 → 热点归因 ① 运行与采样 perf record -F 999 -g -- ./hotspot_demo 产出 perf.data(样本+调用栈) ② 符号与栈解析 perf report --stdio 检查 Overhead / Symbol 失败表现:unknown / [.] 地址 ③ 折叠栈 perf script | stackcollapse 每行 = 一条调用链 + 计数 格式:a;b;c 1234 ④ 火焰图 flamegraph.pl 宽度 ∝ 样本占比 自下而上 = 调用方向 火焰图判读示意(宽度即样本占比,顶层宽块 = 热点) main transform_accumulate(约 88% 样本) checksum sqrt / 除法(浮点运算密集) 数组读写(访存) libm sqrt 调用 判读要点 • 看“宽”不看“高”:横向宽度代表该函数及其子调用占用的样本比例,越宽越热。 • 自下而上读调用方向:main → transform_accumulate → sqrt,热点叶子在顶部。 • 区分“自身耗时”与“子调用耗时”:叶子块宽度才是函数自身开销。 • 若出现大量 unknown 或地址块,先修符号/栈回溯,再谈归因。

执行采样并读取归因数据

先跑一次采样,频率取 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 频率、关闭后台任务后重测
火焰图宽度是采样统计结果,不是精确计时。占比在 1% 以下的块通常受采样噪声影响较大,不宜作为优化首要目标;同时避免在采样期间运行其他重负载程序,否则内核态样本会污染归因。

完成上述步骤后,即得到一份可复用的性能基线:热点函数、占比、调用链与开销性质均已量化。后续任何优化版本都应在相同编译选项、相同输入规模、相同采样频率下重测,并与本基线逐项对比,才能判断优化是否真正生效。