第3章:测量性能
对齐第一版第3章 Measuring Performance:渐近与摊还复杂度、性能属性、可复现实验,以及插桩和采样分析器的证据边界。
学习目标
- 能推导常见算法的渐近复杂度与动态数组扩容的摊还复杂度
- 能设计区分延迟、吞吐、内存和尾部行为的可复现性能实验
- 能比较插桩与采样分析器的观测方式,并从热点证据决定下一步优化
机制总览
第3章:测量性能:机制路径
- 1
从“快”必须有尺度和证据开始
性能不是程序的单一属性。“这个版本更快”至少缺少 workload、input size、hardware、compiler、metric 与 uncertainty。处理十个元素时,常数项较小的线性扫描可能胜过建立索引;数据扩大后,增长率才逐渐支配总成本。服务的平均延迟下降也不代表 p99 改善,…
- 2
渐近复杂度描述增长率
Big O 给出上界增长类别,不是一次调用的耗时。若算法执行 $3n^2 + 7n + 20$ 次基本操作,规模增大时二次项占主导,因此写作 $O(n^2)$。这不表示系数不存在,也不表示所有 $O(n)$ 算法在所有规模都快于 $O(n^2)$;cache locality、allocation、…
- 3
摊还复杂度解释偶发昂贵操作
动态数组 push back 大多只在已分配 storage 尾部构造元素,是常数成本;capacity 用尽时却要申请更大 storage,并移动或复制已有元素。单次扩容可达 $O(n)$,但若 capacity 按几何比例增长,连续 $n$ 次 append 的总迁移量形成几何级数,总工作仍为 …
章级决策实验
第3章:测量性能:机制与证据
切换《第3章:测量性能》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 从“快”必须有尺度和证据开始
性能不是程序的单一属性。“这个版本更快”至少缺少 workload、input size、hardware、compiler、metric 与 uncertainty。处理十个元素时,常数项较小的线性扫描可能胜过建立索引;数据扩大后,增长率才逐渐支配总成本。服务的平均延迟下降也不代表 p99 改善,…
可核验证据
保留可复现基准、输入规模和编译参数,用采样剖析与硬件计数器核对「从“快”必须有尺度和证据开始」前后的时间和资源变化。
学完《第3章:测量性能》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
第3章:测量性能:失效与核验
从“快”必须有尺度和证据开始
典型失效
若脱离基线与成本模型讨论「从“快”必须有尺度和证据开始」,局部优化可能只是在移动开销,甚至让缓存、分配或同步瓶颈更严重。
核验证据
保留可复现基准、输入规模和编译参数,用采样剖析与硬件计数器核对「从“快”必须有尺度和证据开始」前后的时间和资源变化。
渐近复杂度描述增长率
典型失效
若脱离基线与成本模型讨论「渐近复杂度描述增长率」,局部优化可能只是在移动开销,甚至让缓存、分配或同步瓶颈更严重。
核验证据
保留可复现基准、输入规模和编译参数,用采样剖析与硬件计数器核对「渐近复杂度描述增长率」前后的时间和资源变化。
摊还复杂度解释偶发昂贵操作
典型失效
若脱离基线与成本模型讨论「摊还复杂度解释偶发昂贵操作」,局部优化可能只是在移动开销,甚至让缓存、分配或同步瓶颈更严重。
核验证据
保留可复现基准、输入规模和编译参数,用采样剖析与硬件计数器核对「摊还复杂度解释偶发昂贵操作」前后的时间和资源变化。
从“快”必须有尺度和证据开始
性能不是程序的单一属性。“这个版本更快”至少缺少 workload、input size、hardware、compiler、metric 与 uncertainty。处理十个元素时,常数项较小的线性扫描可能胜过建立索引;数据扩大后,增长率才逐渐支配总成本。服务的平均延迟下降也不代表 p99 改善,更不代表 throughput、memory footprint 或 energy 同时改善。
↡描述输入规模趋于增大时,时间或空间成本增长上界、下界或紧确界的方法,忽略与规模无关的常数和低阶项。先预测成本由哪种操作和哪段输入分布支配,再建立可证伪的假设。例如“hash lookup 太慢”应改写成:“在 production key distribution 与 load factor 下,lookup 的 p95 CPU time 由 collision chain 支配;降低 load factor 会减少 cycles/op,但增加 memory。”后者明确了指标、原因、控制变量与可能的取舍。
渐近复杂度描述增长率
Big O 给出上界增长类别,不是一次调用的耗时。若算法执行 次基本操作,规模增大时二次项占主导,因此写作 。这不表示系数不存在,也不表示所有 算法在所有规模都快于 ;cache locality、allocation、branching 与 vectorization 都会改变交叉点。
bool containsPairWithSum(const std::vector<int>& values, int target) {
for (std::size_t i = 0; i < values.size(); ++i) {
for (std::size_t j = i + 1; j < values.size(); ++j) {
if (values[i] + values[j] == target) return true;
}
}
return false;
}最坏情况下检查约 对,是 ;但找到早期匹配时会提前返回,所以实际 cost 依赖 input。要比较替代实现,必须说明 average/worst case、数据分布和是否包含预处理。用 hash set 可把期望时间降到 ,却引入 hashing、allocation 与额外 memory;若 adversarial collision 或 hash guarantee 不成立,不能把期望复杂度说成无条件保证。
O(1)cost does not scale with nWhich hidden state changes the constant?O(log n)discard a fraction each stepIs random access and ordering available?O(n)touch input proportionallyIs the scan contiguous and vectorizable?O(n²)pairwise work dominatesWhere does the target size cross over?空间复杂度同样重要。一个 time-efficient algorithm 可能建立 auxiliary storage;在 memory-bound workload 中,这会增加 cache/TLB pressure,甚至令理论改进反向。复杂度负责排除不随规模成立的方案,benchmark 负责找出目标区间内的真实交叉点。
摊还复杂度解释偶发昂贵操作
动态数组 push_back 大多只在已分配 storage 尾部构造元素,是常数成本;capacity 用尽时却要申请更大 storage,并移动或复制已有元素。单次扩容可达 ,但若 capacity 按几何比例增长,连续 次 append 的总迁移量形成几何级数,总工作仍为 ,所以每次 append 的摊还成本是 。
设 capacity 每次至少翻倍,达到容量 之前被迁移的元素总数满足:
再加上 次新元素构造,总操作数小于 ;因此序列总成本是 ,除以 次 append 得到 amortized time complexity 。这是对整段序列的上界推导,不是一次扩容的耗时预测。
std::vector<Record> records;
records.reserve(expected_count); // known bound: avoid intermediate growth
for (const auto& input : inputs) {
records.emplace_back(parse(input));
}摊还不是概率平均,也不承诺每次调用都快。real-time path 关注 single-operation deadline,偶发 reallocation 仍可能不可接受;此时需要 reserve、fixed-capacity storage 或把 growth 移出关键窗口。反之,盲目 reserve 一个巨大上界会增加 footprint 与 page commitment。正确问题是:sequence-level throughput 还是 per-operation tail latency 是契约?
测量什么:先定义性能属性
↡单位请求或操作从开始到完成的时间,可报告分布、分位数和最大值,而不只报告平均值。latency 回答“一次多久”,throughput 回答“单位时间完成多少”,两者会随 concurrency 与 queueing 相互作用。CPU time 可定位计算消耗,wall time 包含 blocking、I/O 与 scheduling;cycles/op 有助于跨同一机器的运行比较,却不能脱离 frequency 和 microarchitecture。memory 可分 peak resident set、allocation count/bytes、working set 与 bandwidth;还可能关心 startup time、binary size、energy 和 frame-time stability。
稳定系统中可用 Little's Law 检查指标是否自洽:平均在途工作量等于到达率乘平均停留时间。它不是降低延迟的手段,而是提醒我们报告 concurrency、throughput 与 latency 时不能让三者相互矛盾。
测量边界必须与用户可见契约一致。若只计函数 body,却把 input construction、cache warmup 或 output serialization 排除,数字可能回答了另一个问题。microbenchmark 适合隔离机制,component benchmark 观察协作成本,end-to-end test 验证真实路径;三者不是互相替代,而是从因果解释到外部效应逐层补证。
↡单位时间内完成的请求、元素或事务数量,必须同时说明并发度、批大小与背压条件。设计可复现的性能测试
performance testing best practices 的核心首先是实验设计。固定代码与 build flags,记录 CPU、OS、compiler 与 library;选择代表 production 的 input size、shape 和 hit/miss ratio;一次只改变一个待验证因素;用 interleaving 或 randomized order 降低温度、频率和时间漂移对 A/B 的偏置。样本数应由 noise 与所需 detection size 决定,而不是迷信固定次数。
static void BM_lookup(benchmark::State& state) {
const auto table = makeTable(state.range(0), state.range(1));
const auto queries = makeRepresentativeQueries();
for (auto _ : state) {
for (const auto key : queries) {
benchmark::DoNotOptimize(table.find(key));
}
}
state.SetItemsProcessed(state.iterations() * queries.size());
}metric + workload + expected causeinvalid when: vague claim: faster
same build, input, hardware, boundariesinvalid when: confounder changes with the code
distribution + counters + raw samplesinvalid when: best run or mean only
effect size, uncertainty, trade-offsinvalid when: threshold without noise model
编译器会删除无 observable effect 的工作、把 invariant calculation 移出循环,或在 benchmark 与 production 的不同 translation-unit visibility 下产生不同代码。DoNotOptimize/barrier 类工具只能约束特定优化,不能修复不代表生产的 workload。检查 generated assembly 或 counters,确认 timed region 真在执行目标工作;同时把 fixture setup 放到边界外或明确把它纳入指标。
warm cache 与 cold cache 是两个不同场景,不应混成含糊均值。先定义 cache state,再控制或分别报告。frequency scaling、thermal throttling、SMT sibling、NUMA placement、background work 与 allocator state 都可能是 confounder;无法完全消除时,要记录、随机化并报告 distribution。比较结果应给 effect size 与 uncertainty,而不是只给“最快一次”。
热点决定优化上限
↡在目标工作负载中消耗显著执行时间、CPU周期、分配或其他受限资源的代码区域。若函数只占总时间 2%,即使无限加速,端到端改善也不超过约 2%。这就是先 profile 再优化的理由:热点占比给出收益上限,call stack 和 source line 提供归因入口。热点会在优化后转移;改完要重新 profile,不能继续沿用旧画像。
若热点占原执行时间比例为 ,局部加速倍数为 ,端到端加速上限由 Amdahl 关系给出;当 趋于无穷时,总加速仍不超过 。
flat profile 说明样本或时间落在哪个 symbol,call-tree/profile stack 说明它经哪条调用路径到达。inclusive cost 包含 callees,exclusive/self cost 只算函数自身。看到 allocator 热点不等于“allocator 坏”,它可能是上游对象 churn 的结果;看到 memcpy 热点也要追问复制的 owner 和生命周期。证据需要沿调用链回到可修改的 cause。
插桩分析器与采样分析器
↡在函数入口、出口或指定事件处加入探针,记录调用次数、时长或事件数据的分析方式。插桩可由 compiler、binary rewriting 或手动 tracing 实现。它能精确记录被探针覆盖的 calls 与 durations,适合低频路径、调用次数和事件序列;代价是每次事件都执行记录逻辑,可能改变 inlining、cache、timing 与 thread interleaving。细粒度热点若调用极频繁,observer effect 会很明显。
↡按时间或硬件事件周期性中断并收集当前指令地址和调用栈,再用样本比例估计成本分布的分析方式。采样不记录每次调用,而从统计样本估计 CPU time 或 hardware-event 分布,通常开销较低,适合较长运行和近生产 workload。短函数、稀有事件或运行时间太短可能没有足够样本;sampling frequency 越高,分辨率提高但 overhead 也增加。样本比例是估计,不应伪装成精确到纳秒的调用时长。
selected calls and eventscounts, duration, ordering
risk: probe overhead can perturb behavior
use: rare path or focused boundary
periodic instruction + stack sampleslow-overhead cost distribution
risk: short or rare events may be missed
use: long-running CPU hotspot search
sample broadly, instrument narrowlydirection plus causal confirmation
risk: both still observe one workload
use: optimization and regression loop
原书在这一节并列讨论 hot spots、instrumentation profilers 与 sampling profilers。选择方法取决于问题:问“这个 callback 调了几次、顺序如何”偏向 instrumentation;问“长时间 CPU 都花在哪里”偏向 sampling;问 cache miss、branch miss 或 cycles 可用 PMU event sampling/counting。两者可组合:先低开销 sampling 缩小范围,再对少数边界插桩验证调用与 latency。
hypothesis -> representative workload -> baseline benchmark
-> sampling profile -> dominant stack / event
-> focused change -> paired benchmark
-> profile again -> regression guard第3章实验协议
- 为线性扫描、排序与 hash lookup 写出 best/average/worst complexity,并标出假设。
- 记录 vector 连续 append 的 capacity 与单次 latency,区分 spike 和 amortized cost。
- 为同一 workload 同时报告 latency distribution、throughput、allocation 与 peak memory。
- 比较 cold/warm cache 与不同 input distribution,不把两者混成一个均值。
- 检查 benchmark 的 optimized assembly,证明核心计算未被删除或移出 timed loop。
- 用 sampling profile 找出 dominant stack,再用 focused instrumentation 验证调用次数。
- 优化后重新 profile,确认 hotspot 消失、转移或暴露新的 bottleneck。
- 保存 source revision、build flags、environment、raw samples 与 analysis script。
小结
- 渐近复杂度描述规模增长率,不包含常数、cache行为或目标规模的真实交叉点
- 动态数组append可摊还O(1),但单次扩容仍为O(n),tail-sensitive路径必须单独处理
- 测量前先定义latency、throughput、memory、CPU或energy等性能属性与边界
- 可信测试需要代表性workload、相同build、控制变量、有效timed region和uncertainty
- hotspot给出潜在收益上限,inclusive/exclusive cost与调用栈帮助追到原因
- instrumentation精确覆盖事件但可能扰动程序;sampling以较低开销估计成本分布
- 性能工程是“假设、测量、profile、修改、复测、再profile”的闭环
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
测量什么:机制、边界与证据
在《第3章:测量性能》的官方单元 chp-03 中,测量什么连接本章第 3 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 渐近复杂度
- 描述成本随输入规模增长的类别。
- Big O notation
- 表达渐近增长上界的记号。
- 摊还复杂度
把操作序列总成本分摊到每次操作的保证。
- latency
- 一次操作从开始到完成的时间分布。
- throughput
给定并发和背压条件下的单位时间完成量。
- hotspot
目标工作负载中消耗显著受限资源的代码区域。
- instrumentation profiler
通过入口出口或事件探针记录执行的分析器。
- sampling profiler
周期采样指令位置和调用栈来估计成本分布的分析器。
练习
- 问题 1:解释vector append为何摊还O(1),并为16 ms帧预算设计无扩容方案。 区分操作序列保证与单次尾延迟。
- 问题 2:一个查找优化平均快12%,如何设计实验判断它是否真实且值得上线? 同时考虑尾部、内存和输入分布。
- 问题 3:采样显示allocator占CPU样本25%,下一步应怎样验证原因? 说明为什么不能直接替换allocator。