2408 字
约 8 分钟
14
S27-A19 · 实测的样本量:跑几轮才算够

一次对比测试跑 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. 自测

  1. 三个阶段里哪一个最常被跳过?"同方案 10 轮"能测出什么?
  2. 当目标差异远小于噪声时,为什么"加轮次"不是最优解?
  3. 均值、P50、P99 分别适合什么?为什么"对比典型表现"该用 P50?
  4. "交替执行"解决了哪种偏差?为什么它不增加成本?
  5. 为什么"差异小于噪声时"要写"未观察到差异"?写成"略慢"会怎样?
  6. 判断"三轮够不够"的那个问题是什么?
S27-A19 · 实测的样本量:跑几轮才算够
https://www.clxhxhhr.top/posts/6553/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。