Skip to content

第18章:性能工程


元数据卡

属性
难度Lv.3(需 ch17 调用约定基础)
核心工具perf / gprof / Valgrind / FlameGraph
关键定律Amdahl's Law
代价维度CPU / 内存 / 分支预测 / 缓存
前置章节Vol 3 ch15(缓存)、ch16(链接)、ch17(调用约定)

你在哪

"你已经探索了地心的每个角落。现在你不再是学习者,而是工程师——能用 profiler 分析瓶颈、用缓存友好代码榨干每一丝性能。最后一章,把学到的所有知识用在优化上。"

读完前几章,你已经能看懂汇编、理解栈帧、知道缓存行是什么。但问题来了:

你的程序跑得慢。你改了一处"看起来慢"的代码,性能没变。你改了好几个地方,反而更慢了。

原因:你猜的。你凭直觉优化了,但直觉在性能工程面前一文不值。

性能工程的核心不是"写快代码"——是先测量,再动刀。这一章围绕三个核心工具展开:

工具用途来源
perf看 CPU 在干啥(热点/缓存未命中/分支预测失败)Linux 内核自带
火焰图把热点可视化成一堆叠起来的矩形Brendan Gregg
Amdahl 定律算清楚"优化这个到底能快多少"Gene Amdahl

核心原则:优化前先证明瓶颈在哪,优化后证明它真的被解决了。


你的任务

本章分层

  • 必读:性能工程的黄金法则(先测量,再优化);perf stat 的基本使用;Amdahl 定律的含义与局限性
  • 选读perf record + 火焰图的生成与解读;微基准测试的五种常见陷阱
  • 深水区:缓存行 bouncing 的识别与修复;Callgrind 分支预测分析;分支布局对 i-cache 的影响 本章不会要求你掌握
  • Intel PEBS 精确事件采样机制
  • Roofline 模型的内存带宽分析
  • eBPF 动态跟踪
  1. 能用 perf statperf record/report 定位性能瓶颈
  2. 看懂并生成火焰图(stack collapse → flamegraph.pl)
  3. 识别微基准测试的 5 种陷阱(JIT warmup / 分支偏差 / 编译器欺骗 / 死代码消除 / 内联污染)
  4. 掌握 4 种性能反模式(假共享 / 分配风暴 / 错误的分支布局 / 缓存行 bouncing)
  5. 理解 Amdahl 定律的极限含义:串行部分决定天花板

遭遇战 → 获得技能

遭遇战 #1:perf stat — 你猜的和真实的不一样

代码:一个简单的循环求和

c
long sum(int *arr, size_t n) {
    long s = 0;
    for (size_t i = 0; i < n; i++)
        s += arr[i];
    return s;
}

运行后你觉得瓶颈是"加法指令太多"。但 perf stat 告诉你:

bash
perf stat ./a.out

 Performance counter stats for './a.out':

      1,847,621,340      cycles                    # 3.27 GHz
        723,419,812      instructions              # 0.39  insn per cycle
          4,529,318      branch-misses             # 3.2%  of all branches
        312,419,234      L1-dcache-load-misses     # 23.8% of all L1 loads

瓶颈不是加法——是缓存未命中(23.8%)。你的 CPU 在等内存。

技能养成:永远先跑 perf stat 看宏观指标(CPI / 缓存命中率 / 分支预测),再决定优化的方向。

遭遇战 #2:perf record + 火焰图

bash
perf record -F 99 -g -- ./a.out    # 99Hz 采样,带调用栈
perf script > out.perf             # 转成可读格式
stackcollapse-perf.pl out.perf > out.folded   # 折叠栈
flamegraph.pl out.folded > flame.svg          # 生成火焰图

读图规则

  • 每个矩形 = 一个函数调用
  • 宽度 = 占总 CPU 时间的比例
  • 颜色无意义(默认随机暖色)
  • 鼠标悬停看函数名和占比
  • 最宽的顶层矩形就是性能热点——从这里开始优化

不需要火焰图生成脚本?perf report -g 看文本版一样可以找到热点函数。

遭遇战 #3:微基准测试的 5 个陷阱

c
// ❌ 陷阱代码:测试 vector 和 list 插入性能
#include <benchmark/benchmark.h>
#include <vector>
#include <list>

static void BM_VectorInsert(benchmark::State &state) {
    for (auto _ : state) {
        std::vector<int> v;
        for (int i = 0; i < state.range(0); i++)
            v.insert(v.begin(), i);  // O(n)
    }
}

这个基准测不是"vector 前插有多慢"——它测的是测试本身的内存分配代价加上 vector 移位再加上编译器是否看穿了你的意图

5 个必知陷阱

#陷阱表现解法
1JIT warmup(Java/PyPy)前几千次调用很慢,后面突然快先跑预热循环,不计入测量
2分支预测偏差测试数据是排序的 → 分支预测 99% 命中 → 上线时随机数据 → 慢了 5×用真实分布的数据集
3编译器欺骗编译器发现计算结果没用 → 整个循环删掉了benchmark::DoNotOptimize() 或把结果写进 volatile 变量
4死代码消除开了 -O2,你测的 "empty loop overhead" 是 0同上,或把结果累加到 static 变量
5内联污染被测试函数被内联到了基准框架中,测量的是合并后的代码__attribute__((noinline)) 强制不内联,或把函数放另一个编译单元

💡 黄金准则:任何微基准测试如果不开优化(-O2)跑,结论就是废纸。开了优化又需要小心陷阱 3 和 4。

遭遇战 #4:Amdahl 定律

公式:

$$S = \frac{1}{(1 - p) + \frac{p}{N}}$$

其中:

  • $p$ = 可以并行化的比例
  • $N$ = 核心数
  • $S$ = 加速比

残酷结论:当 $N \to \infty$,$S \to 1/(1-p)$。串行部分决定天花板。

例子:你有一部分串行代码占 5%,并行部分可以无限多核。极限加速比 = 1 / 0.05 = 20 倍。扔 1000 个核也没用。

实际应用:优化之前,先问:"这段代码有多少比例是本质串行的?" 如果答案是 > 10%(也就是加速天花板 < 10 倍),先别忙加核,先优化串行路径。


常见陷阱

进阶排障:缓存行 bouncing(或 False Sharing)是在多线程场景下才会触发的性能问题。以下实验需要 4 核以上机器才能观察到明显差异。

实验 1:找出一个真实缓存行 bouncing

c
// 假共享(False Sharing)——性能杀手 #1
struct alignas(64) Counter {
    volatile long value;
};

Counter counters[4];

void worker(int id) {
    for (int i = 0; i < 100000000; i++)
        counters[id].value++;  // 不同核写不同 Counter
}

// 如果不加 alignas(64),多个 Counter 在同一个缓存行上
// 每次写都会让其他核的缓存行失效 → cache coherence 流量爆炸
// perf stat 会看到巨大的 LLC-load-misses

perf stat -e cache-misses,LLC-load-misses,LLC-store-misses 对比加/不加 alignas(64) 的性能差异。4 个线程下,不加可能慢 10–100 倍

修复:每个线程私有的变量,写完再聚拢。

进阶排障:Callgrind 是 Valgrind 套件的指令分析工具。以下实验面向需要精细分析分支预测行为的性能工程师。

实验 2:Callgut

callgrind_annotate --auto=yes callgrind.out.XXXX


看 `Ir`(指令数)和 `Bcm`(分支预测失败次数)。如果 `Bcm` 占比 > 5%,考虑用**分支消除**(branch elimination)技术——比如用查表代替 `if-else`,或用 SWAR(SIMD Within A Register)。

>  **进阶排障**:分支布局优化是编译器后端熟知的技巧。以下内容面向需要对微架构级性能做调优的开发者。


### 实验 3:分支布局反模式

```c
// ❌ 反模式:错误的分支布局
// 如果 error 几乎从不发生,条件应该写成:
if (likely(!error)) {
    // 正常_builtin_expect(C 风格)
// 或 [[likely]] / [[unlikely]](C++20)
if (error) [[unlikely]] {
    // 编译器会把 error 分支放到远离正常路径的地方
    // 改善 i-cache 局部性
}

通关挑战

  1. 写一个随机数生成 + 排序的程序。用 perf stat 找出排序阶段的 CPI,再用 perf record + 火焰图定位热点函数
  2. Amdahl 计算:一个 pipeline 有 4 个阶段,分别耗时 40%、25%、20%、15%。只能并行化阶段 2 和 3(各 4 核),最大加速比是多少?
  3. 写两个版本的快排:一个用 if (arr[i] < pivot) 分支,一个用 CMOV(条件移动指令)。开 -O2 对比吞吐量
  4. 用 Valgrind Callgrind 检查你的代码中分支预测失败率最高的 3 个函数
  5. 构造一个假共享 benchmark,证明 alignas(64) 前后的性能差异(≥4 线程)

验收标准

要求自检
能用 perf stat 读 CPI 和缓存未命中率perf stat ./a.out
能生成并阅读火焰图栈折叠 → flamegraph.pl
能识别 5 种微基准测试陷阱JIT warmup / 分支偏差 / 编译器欺骗 / DCE / 内联污染
能用 Amdahl 定律做工程决策串行比例决定极限加速比
能识别假共享并修复alignas(64) 隔离缓存行
能解释 __builtin_expect / [[likely]] 的原理编译器调整分支布局,改善 i-cache

常见卡点

症状病因
perf 说瓶颈是 A,优化 A 后没变化有隐藏瓶颈 B(I/O / 锁竞争 / 系统调用)——perf 默认不跟踪 syscall softirq
火焰图全是 __memset_avx2内存分配是瓶颈。检查你是否频繁 new/delete
微基准结果和线上不一致线上有分支预测偏差和缓存污染,基准是"干净"的
Amdahl 算出来加速 10 倍,实测只有 2 倍忽略了同步开销、内存带宽、缓存一致性协议
火焰图显示 libc_start_main 最宽可能只采样了初始化/清理阶段,循环没跑够时间

现在不需要理解

  • Intel PEBS 精确事件采样(perf 底层机制)
  • Roofline 模型(内存带宽 vs 计算强度的权衡——Vol 4 的"性能建模"会讲)
  • eBPF 动态跟踪(深水区 profiling 技术)
  • C++ 的 std::hardware_destructive_interference_size(C++17 缓存行对齐工具)

旅人笔记

性能工程里最难的从来不是写快代码——是先证明哪里慢。你已经掌握了三种方法:

  • perf 告诉你"事实"
  • 火焰图告诉你"哪里"
  • Amdahl 告诉你"能快多少"

剩下的就是迭代:测量 → 假设 → 改代码 → 再测量 → 确认或推翻。这个循环比你猜一万次都有效。

记住数据压倒直觉。每次有人说"我觉得 X 是瓶颈",你的标准回答应该是:"让我们跑一遍 perf stat 再说。"


→ 下一站预告:走出地心,踏上网络之路

你爬出计算机地心的洞口,站在地面上。变量村的方向炊烟袅袅,但你没有回头。老陈很久以前就说过——"外面很大。" 通往其他村庄、其他世界的路,就在前方。你叫它——网络。

Vol 3 带你钻进了计算机的地心。你从晶体管焊在了硅片上(ch1),一路看到了 CPU 怎么取指令(ch8–ch9)、缓存怎么藏数据(ch15)、链接器怎么把你的 .o 拼成可执行文件(ch16)、汇编怎么传参数(ch17),最后学会了怎么找到它慢在哪(ch18)。

但你的程序不是孤岛。

下一卷,Vol 4:计算机网络,我们从沙盒里走出来,从网络分层模型到 TCP 拥塞控制,从 socket 编程到 QUIC 和 HTTP/3——你写的每一行网络代码,最终都会变成一次中断、一次 DMA、一块网卡缓冲区里的比特流。

下一站:网线那头,世界正等着你的数据包。


"The first step of optimization is to stop guessing. The second step is to measure." —— Brendan Gregg

Built with VitePress | Software Systems Atlas