一次对比测试跑 3 轮,A 方案 118ms、B 方案 121ms,结论写"B 略慢"。第二次跑 3 轮,结果反过来:A 122ms、B 117ms。而两个方案的实际差别是零——3 轮的结果全在噪声里,而两次测试的"结论"却相反。
样本量不是"越多越好"的教条,而是一个可算的量:你想要的"能分辨的差异",与系统的"噪声水平"决定了最小轮次。这个顺序很容易被颠倒:多数人先跑对比、再发现"结论不稳",于是返工重测。而"先花十分钟测噪声"能省掉这次返工。
而"噪声水平"要靠第一次测试量出来。
1. 三个阶段
| 阶段 | 目的 |
|---|---|
| 先测噪声 | 知道"波动多大" |
| 再定样本量 | 按目标差异算轮次 |
| 再看结果 | 与噪声比较 |
第一阶段最常被跳过:**没测过"同一方案自己跟自己比"**第一阶段的产出也很实用:它给出"这台机器能分辨多小的差异"。而这台机器的噪声水平会随"是否有其它负载"变化——所以它应当每次测(十分钟)。
,于是不知道"118 与 121 的差"是信号还是噪声。
这一步很便宜:同一个方案跑 10 轮,看最小值与最大值差多少。
// 噪声测量(同方案 10 轮)
P50 = 120ms, 最小 = 112ms, 最大 = 133ms
噪声幅度 ≈ (133-112)/120 ≈ 17%
// 结论:目标差异若小于 17%,必须靠增加轮次或改进测量(减噪)
2. 怎么算"够不够"
| 目标差异 | 需要的轮次(经验) |
|---|---|
| > 噪声幅度 | 3–5 |
| ≈ 噪声幅度 | 10–20 |
| < 噪声幅度的一半 | 需要先减噪,不是加轮次 |
| 想分辨 5% 以内 | 通常要"改测量方法" |
第三项的量化关系大致是"所需轮次 ∝ (噪声/目标差异)²"。所以"噪声是目标的 3 倍"意味着需要约 9 倍轮次,而不是 3 倍。
第三项是关键判断:当目标差异远小于噪声时,加轮次只是在"平均掉"噪声,而它需要成倍增加(大致与方差的平方成正比);此时更有效的是"减少噪声来源"。
减噪的手段:固定 CPU 频率、清缓存、隔离其它负载、用 CPU 时间代替墙钟时间。第四项(CPU 时间)在"多任务环境"里最有效:墙钟时间包含"等 CPU"的时间,而 CPU 时间只算"真的在用"。于是"同一段代码"在负载高的机器上墙钟翻倍,而 CPU 时间几乎不变。
样本量还有一个"预热"陷阱:前几轮永远偏慢(JIT、缓存、连接池),所以"3 轮取 P50"里若包含预热轮,P50 会被拉高。正确做法是"固定丢弃前 N 轮",N 由"何时稳定"决定。
// 预热判定(跑 10 轮,看何时进入平台区)
轮次: 1 2 3 4 5 ...
耗时: 210ms 140ms 125ms 121ms 119ms ...
// 结论:前 2 轮为预热,丢弃;有效样本从第 3 轮起
3. 三种"平均值"的差别
| 统计量 | 用途 |
|---|---|
| 均值 | 受长尾影响大 |
| 中位数(P50) | 稳健,推荐 |
| P99 | 看长尾 |
第一项在"有偶发尖刺"时失真:10 次里 1 次 500ms、9 次 100ms,均值 140ms,而它不代表任何一次的真实体验。
第一项(均值)在"分布对称且无尖刺"时才接近 P50。而"有 GC、有锁、有网络抖动"的系统几乎从不对称——所以"用均值对比"是这类测试最常见的统计错误。
第二项是"对比测试"最该用的:它不受个别尖刺影响,而"两个方案的典型表现"正是你要比的。
// 同一组数据的三种统计
样本:100,102,101,99,500(第 5 次被 GC 打断)
均值 180.4 ← 被尖刺拉高
P50 101 ← 反映典型
P99 500 ← 反映长尾
// 结论:对比"典型表现"用 P50;评估"用户体验"看 P99
4. 交替执行,而不是分批
| 方法 | 偏差 |
|---|---|
| 先跑 A 10 轮,再跑 B 10 轮 | 环境漂移(温度、负载) |
| 交替 A/B/A/B… | 偏差被摊平 |
| 随机顺序 | 更好 |
第一项的问题很实际:跑 A 的时候机器还凉,跑 B 时已经热了,于是"B 慢"里混进了"温度效应"。
第二项能把"时间相关的漂移"平均到两边,而它不需要额外成本——只是换个执行顺序。第三项(随机顺序)比"严格交替"更稳:交替仍可能与"某个周期性任务"共振(每两轮撞一次),而随机能打散这种节律。代价是"轮次不等"时需要更仔细的统计。
// 交替执行(各 10 轮,交错)
for i in $(seq 1 10); do
run_A >> a.txt
run_B >> b.txt
done
// 然后各自取 P50 比较
"轮次"还要与"每轮时长"一起看:10 轮 × 1 秒 与 3 轮 × 10 秒 都是 10 秒总时长,但后者对"长尾"更敏感(能看到更多 GC/抖动周期)。
// 两种配置的适用
10 轮 × 1 秒:测"典型处理速度",适合对比算法
3 轮 × 10 秒:测"长尾与稳定性",适合评估线上表现
// 判据:你要看"中位"还是"长尾"
5. 结果怎么报
| 内容 | 必须写 |
|---|---|
| 轮次 | 各跑了几轮 |
| 统计量 | P50/P99/均值 |
| 噪声幅度 | 同方案的波动范围 |
| 结论强度 | "差异显著"还是"在噪声内" |
第四项与"可信度"直接相关:一份"诚实地写了在噪声内"的报告,比一份"果断给出 2.5% 优势"的报告更可信——因为后者不知道何时会翻车。
第四项是"诚实"的关键:当差异小于噪声时,应当写"未观察到差异",而不是"B 略慢"。后者会被引用成一个"结论"。
// 两种写法(同一组数据)
差:B 比 A 慢 2.5%(121ms vs 118ms)
好:A/B 各 10 轮交替执行。P50 分别为 119ms 与 120ms,同方案噪声幅度 17%。
差异(0.8%)远小于噪声 → 未观察到可分辨差异。
6. 排错:五种"样本量问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 两次测试结论相反 | 轮次不足 | 先测噪声 |
| 组内方差很大 | 有外部干扰 | 隔离负载 |
| 差异"稳定"但很小 | 可能在噪声内 | 与噪声比较 |
| 越跑越慢 | 累积状态(日志/磁盘) | 每轮重置环境 |
| 报出的数比谁都好 | 只报了最好一次 | 报 P50 |
第四项在"长时间测试"里常见:日志文件增长、内存碎片、连接池状态都会让后面的轮次变慢第五项(只报最好一次)是"选择性报告",而它很难被察觉:读者只看数字,不知道"这是 10 次里最好的"。所以"报 P50 + 噪声幅度"是一种自我约束。
,于是"轮次越多均值越差"。
最后一个"组织"实践:把"噪声幅度"写进测试脚本的输出,而不是靠人记得测。这样每次测试都会顺带报出"这台机器当前能分辨多小的差异",而"结论强度"就变成一个可计算的判断,而不是印象。
// 脚本输出(自动附带噪声)
baseline_noise: 17% (same-config 10 rounds)
A: p50=119ms B: p50=120ms delta=0.8% → within noise (not comparable)
// 收益:任何人拿到这份输出,都能独立判断"这个结论能不能用"
7. 边界:什么情况三轮就够
| 场景 | 为什么 | 做法 |
|---|---|---|
| 差异 > 2 倍噪声 | 信号明显 | 3 轮 |
| 只看"数量级" | 不需要精确 | 1–3 轮 |
| 功能正确性验证 | 与性能无关 | 1 轮 |
| 已有他人基准可参考 | 只需复现趋势 | 3 轮 |
最后一条与"决策"有关的边界:当差异落在噪声内时,选择依据应当转向"非性能因素"(可维护性、生态、团队熟悉度)。这也是一种结论——"性能不是决策因素",而不是"没测出来"。
判断口径一句话:"我想要的差异"与"同方案自身的波动"哪个大。差异明显大于波动,少量轮次就够;接近或小于,就必须先减噪或大幅加轮次。
8. 自测
- 三个阶段里哪一个最常被跳过?"同方案 10 轮"能测出什么?
- 当目标差异远小于噪声时,为什么"加轮次"不是最优解?
- 均值、P50、P99 分别适合什么?为什么"对比典型表现"该用 P50?
- "交替执行"解决了哪种偏差?为什么它不增加成本?
- 为什么"差异小于噪声时"要写"未观察到差异"?写成"略慢"会怎样?
- 判断"三轮够不够"的那个问题是什么?