|
|
很多人做 C++ 项目到后期都会遇到同一个问题:程序功能都跑通了,但线上 CPU 占用莫名其妙,延迟偶尔抖一下,团队里开始靠“感觉”优化。这时候别再拍脑袋,拿起 perf 和火焰图,一周内能把主要瓶颈掀个底朝天。
先说 perf。它是 Linux 自带的性能分析“瑞士军刀”,不花里胡哨,但很硬核。核心思路是基于采样:以固定频率中断收集栈信息,统计哪些符号最常出现。实操里我一般这么走:编译带上 -g 保留符号,关闭/减小 -fomit-frame-pointer(或在 x86_64 上加 -fno-omit-frame-pointer,便于栈回溯),线上跑一段稳定流量,然后 perf record -F 99 -g -p 抓 30-60 秒。采样频率 99Hz 对多数 CPU 开销可控,-g 打开调用栈。抓完用 perf report 看热点,能迅速确认“时间花在哪”。
但 perf report 的层级列表看久了会眼花,识别路径关联不直观,所以我更喜欢把数据转成火焰图。Brendan Gregg 的工具链很顺手:perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg。火焰图里每个方块代表函数,宽度是“时间占比”,从下往上是调用栈,左右没意义。它的优势在于:一眼看到“整片宽”的热点层,能直观区分是算法热点、锁竞争还是系统调用堆积。工具在 https://www.brendangregg.com/flamegraphs.html,有现成脚本。
具体案例感受最强。之前一个高并发服务,团队怀疑 JSON 解析慢。我用火焰图一看,解析只占一小条,真正的大头是 std::unordered_map 的 rehash,宽得离谱。顺藤摸瓜发现 key 设计导致海量冲突,负载因子被动偏高。两步优化:预留容量和换成更适合的 robin-hood 哈希,实现上不难,CPU 立减 20%。这类“认知偏差纠正”是火焰图最大的价值。
还有几条踩坑提醒。第一,符号表必备,不然火焰图只剩一堆地址;线上容器镜像要带上符号或用 debuginfo 分离包。第二,内联与优化会让调用栈“扁平化”,可在关键模块上用 noinline/冷热点分离做观测窗。第三,C++ 异常与自定义协程栈会破坏栈回溯,必要时用 frame pointers 和合适的 unwind 信息,或切到 eBPF-based 方案(如 perf + modern kernel,或尝试 https://github.com/brendangregg/FlameGraph 页面里提到的 On-CPU/eBPF 采样)。第四,多线程程序要分场景抓数据,混采可能把锁竞争稀释;可以按 pid/tid 精准挂载,或者在压测流量下对单实例定向采样。
别忘了 off-CPU 视角。有时延迟不是算力瓶颈,而是 I/O 等待或锁等待。perf 自身能抓调度事件,配合 off-CPU 火焰图把睡眠栈画出来;当你看到某把大锁像城墙一样横在图上,就知道优化方向是细化锁、缩短临界区,或改成无锁结构。
最后,流程化很关键。我的做法是:基准场景固定化(请求样本和速率)、采样参数模板化(频率、时长、过滤条件)、结果归档(SVG 加注释和 git 提交),每次改动都用同等手段复测。别追求一次定位所有问题,80/20 原则先干掉最宽的那块。等你第一次把“拍脑袋优化”换成“证据驱动”,团队对性能的讨论维度就会完全不同。 |
|