16. 性能分析工具实战:使用 perf 定位热点函数、使用火焰图分析性能瓶颈
性能优化这件事,说白了就是「找到最慢的那块代码,然后干掉它」。但问题来了——你怎么知道哪块代码最慢?凭感觉猜?我年轻时干过这种事,结果优化了一整天,发现瓶颈在另一个完全没想到的地方。
所以,工具很重要。今天咱们就聊聊两个实战利器:perf 和 火焰图。一个负责采样数据,一个负责把数据变成你能看懂的图。
16.1 perf:Linux 性能分析的瑞士军刀
perf 是 Linux 内核自带的性能分析工具。它不依赖任何第三方库,装上内核就能用。我个人习惯把它当作性能优化的「第一站」——先跑一轮 perf,看看全局情况。
16.1.1 基本用法
先来个最简单的例子。假设你有一个程序叫 myapp,你想看看它运行时 CPU 都在忙什么:
# 采样 10 秒,记录 CPU 周期事件
perf record -e cycles -a -g -- ./myapp
# 生成报告
perf report
这里几个参数我解释一下:
-e cycles:指定采样事件为 CPU 周期。你也可以换成cache-misses、branch-misses等。-a:对所有 CPU 核心采样。如果你只想跟踪某个进程,用-p PID。-g:记录调用栈。这个很重要,没有它你就不知道热点函数是谁调进来的。
跑完之后,你会看到一个交互式界面。按上下键浏览,按回车展开调用链。嗯,这里要注意:如果函数名显示为 0x7f1234...,说明你的程序没有加 -g 编译选项,或者 strip 掉了符号表。
-g -fno-omit-frame-pointer。不加的话,perf 拿不到完整的调用栈,火焰图也会缺胳膊少腿。我曾经因为这个浪费了一整个下午。
16.1.2 常用事件类型
perf 支持的事件非常多。我列几个最常用的:
| 事件名 | 说明 | 适用场景 |
|---|---|---|
cycles |
CPU 周期数 | 通用热点分析 |
instructions |
执行的指令数 | 看代码效率 |
cache-misses |
缓存未命中次数 | 内存访问优化 |
branch-misses |
分支预测失败次数 | 条件分支优化 |
context-switches |
上下文切换次数 | 锁竞争、IO 阻塞 |
你可以组合多个事件一起采样:
perf stat -e cycles,instructions,cache-misses ./myapp
这个命令会直接输出统计结果,不生成数据文件。适合快速验证优化效果。
16.2 火焰图:把性能数据变成「温度图」
perf report 虽然能用,但说实话,看多了眼睛疼。尤其是调用链一深,满屏的缩进让人头大。这时候火焰图就派上用场了。
火焰图是 Brendan Gregg 发明的可视化工具。它的原理很简单:横轴是采样比例,纵轴是调用栈深度。每个矩形代表一个函数,宽度越宽,说明它占用的 CPU 时间越多。
16.2.1 生成火焰图
你需要先安装 FlameGraph 工具集:
git clone https://github.com/brendangregg/FlameGraph.git
cd FlameGraph
然后三步走:
# 第一步:用 perf 采集数据
perf record -F 99 -a -g -- ./myapp
# 第二步:生成折叠的调用栈文件
perf script | ./stackcollapse-perf.pl > out.folded
# 第三步:生成 SVG 火焰图
./flamegraph.pl out.folded > flame.svg
这里 -F 99 表示每秒采样 99 次。为什么是 99?不是 100?因为 100 可能和某些内核定时器产生共振,导致采样偏差。这是 Brendan Gregg 的建议,我试过确实有效。
perf record -F 99 -a -g -- sleep 10 配合 ./myapp & 后台运行。这样采样时间更可控。
16.2.2 怎么看火焰图
拿到 SVG 文件后,用浏览器打开。你会看到一张像火焰一样的图:
- 顶部是正在运行的函数,底部是调用入口(通常是
main或_start)。 - 矩形越宽,说明这个函数占用的 CPU 越多。这就是你的优化目标。
- 鼠标悬停会显示函数名和采样占比。
- 点击某个矩形可以放大查看它的子调用链。
我一般先看最宽的「平顶」——就是顶部那些特别宽的矩形。它们往往是热点函数。如果某个函数占了 30% 以上的宽度,那基本就是瓶颈了。
16.3 实战案例:优化一个字符串处理程序
光说不练假把式。咱们来个真实案例。
假设有这样一个程序:它从文件里读入大量字符串,然后做某种转换。跑起来很慢,用户抱怨说处理 1GB 数据要 5 分钟。
// 简化版代码
std::vector<std::string> process_lines(const std::vector<std::string>& lines) {
std::vector<std::string> result;
for (const auto& line : lines) {
std::string trimmed = trim(line);
std::string upper = to_upper(trimmed);
result.push_back(upper);
}
return result;
}
先用 perf 看看:
perf record -F 99 -a -g ./myapp
perf report
报告显示,to_upper 占了 42% 的 CPU 时间,trim 占了 31%。嗯,意料之中。但等等——std::string 的拷贝构造函数也占了 12%?这不对劲。
生成火焰图看看:
perf script | ./stackcollapse-perf.pl > out.folded
./flamegraph.pl out.folded > flame.svg
打开 SVG,我一眼就看到 std::string::operator= 和 std::string::string 在火焰图顶部占了不小的宽度。这说明什么?说明字符串拷贝太多了。
优化方案其实很简单:
- 用
std::string_view代替std::string作为中间结果,避免拷贝。 - 用
reserve预分配内存,减少 realloc。 - 把
to_upper改成原地修改,而不是返回新字符串。
改完之后再跑一次 perf:
perf stat -r 5 ./myapp_optimized
结果:处理时间从 5 分钟降到了 1 分 20 秒。火焰图上 to_upper 的宽度从 42% 降到了 18%。
16.4 常见坑与避坑指南
用 perf 和火焰图的时候,有几个坑我踩过,写出来给你提个醒:
- 采样频率别太高:
-F 9999看起来很猛,但会导致 perf 自身占用大量 CPU,反而干扰结果。99 或 199 就够用。 - 短程序别用 record:如果程序跑不到 1 秒,采样点太少,结果没意义。可以用
perf stat代替。 - 火焰图不是万能的:它只显示 CPU 时间。如果你的程序在等 IO、等锁、等网络,火焰图上看不到。这时候要用
off-cpu火焰图或perf trace。 - 别忘了内核符号:如果热点在内核里(比如系统调用),需要 root 权限才能看到内核符号表。加
sudo跑 perf。
我曾经有一次优化一个网络服务,火焰图显示 recvfrom 占了 60%。我以为是内核问题,折腾了半天。后来才发现是应用层缓冲区太小,导致频繁系统调用。嗯,这个教训挺深刻的。
16.5 知识体系总览
下面这张图总结了 perf 和火焰图的核心流程:
这张图展示了完整的分析链路。从左到右,从采集到可视化,每一步都有对应的工具。你不需要每次都走完全流程——如果只是快速看看热点,perf report 就够了。但要做深入分析,火焰图是更好的选择。
16.6 写在最后
perf 和火焰图是我日常优化工作中最常用的组合。它们免费、高效、社区活跃。你不需要花大价钱买商业工具,也能把性能优化做到位。
记住一句话:没有数据,就没有优化。下次你觉得程序慢,别急着改代码。先跑一轮 perf,看看数据怎么说。
公众号:蓝海资料掘金营,微信deep3321