stats 教程
本教程把计时转化为站得住脚的决策:稳健的摘要、基于实际阈值的成对比较、可复现的自助法区间,以及不触动原始数据的、用于图表的清理后视图。示例都是完整的测试;把其中一个粘贴到导入了下面所示模块的包中,然后运行 moon test。
快速上手
添加模块并导入这两个包:
moon add Luna-Flow/mare_mark@0.3.0
import {
"Luna-Flow/mare_mark/model",
"Luna-Flow/mare_mark/stats",
"Luna-Flow/mare_mark/event",
"Luna-Flow/mare_mark/runner",
"moonbitlang/async",
}
把候选与在相同八个区组中测量的基线进行比较:
test "is the candidate at least 2 % faster?" {
let baseline = [100.0, 102.0, 98.0, 101.0, 99.0, 103.0, 97.0, 100.0]
let candidate = [90.0, 93.0, 88.0, 92.0, 91.0, 94.0, 87.0, 90.0]
let result = @stats.compare_paired(
"baseline", "candidate", baseline, candidate, 2.0, @model.confirmatory_interval(),
)
inspect(result.relative_delta_pct, content="-9.5")
inspect(@stats.is_faster(result), content="true")
}
成对差值的中位数是基线中位数的 9.5 %,远超 2 % 的阈值,因此决策为 Faster。
日常任务
总结一个实现
summarize 并列给出经典统计量和稳健统计量。当均值和中位数不一致时,样本中存在值得查看的尾部。
test "summarize timings" {
let timings = [12.0, 12.5, 11.75, 12.0, 12.25, 31.5, 12.25, 12.0]
let summary = @stats.summarize(timings)
inspect(summary.median, content="12.125")
inspect(summary.mean, content="14.53125")
inspect(summary.iqr, content="0.3125")
inspect(summary.max, content="31.5")
}
一次 31.5 µs 的运行把均值拉到 14.5 µs;中位数仍保持在 12.1 µs。请报告中位数和 IQR,并把最大值作为诊断量保留。
用实际阈值做决策
阈值表明你所关心的最小变化。1 % 的改进在 0.5 % 的阈值下为 Faster,在 2 % 的阈值下为 Equivalent:
test "the threshold decides what counts" {
let baseline = [100.0, 100.0, 100.0, 100.0]
let candidate = [99.0, 99.0, 99.0, 99.0]
let strict = @stats.compare_paired(
"a", "b", baseline, candidate, 0.5, @model.confirmatory_interval(),
)
let lenient = @stats.compare_paired(
"a", "b", baseline, candidate, 2.0, @model.confirmatory_interval(),
)
inspect(strict.decision is Faster, content="true")
inspect(lenient.decision is Equivalent, content="true")
}
请在查看数据之前选定阈值,并把它与结果一起记录。
添加可复现的区间
compare_paired_with_bootstrap 保留决策,并把四分位区间替换为中位数差值的百分位自助法区间。固定种子和重抽样次数,使重新生成报告时得到相同的数字:
test "bootstrap interval of the median delta" {
let baseline = [100.0, 102.0, 98.0, 101.0, 99.0, 103.0, 97.0, 100.0]
let candidate = [90.0, 93.0, 88.0, 92.0, 91.0, 94.0, 87.0, 90.0]
let first = @stats.compare_paired_with_bootstrap(
"baseline", "candidate", baseline, candidate, 2.0,
@model.confirmatory_interval(), 2026UL, 2000, 95.0,
).unwrap()
let again = @stats.compare_paired_with_bootstrap(
"baseline", "candidate", baseline, candidate, 2.0,
@model.confirmatory_interval(), 2026UL, 2000, 95.0,
).unwrap()
inspect(first.interval.low, content="-10")
inspect(first.interval.high, content="-9")
inspect(first.interval.low == again.interval.low, content="true")
}
区间的单位是 µs,即输入的单位:在 95 % 的自助法置信度下,中位数差值位于 −10 µs 与 −9 µs 之间。
对一次运行中的观测进行配对
运行器为每个实现和区组发出一个 Observation。按 block_id 配对,只保留有效的验证性观测,然后进行比较。本示例运行的是真实的基准测试,因此只展示不取决于机器的数字:
fn paired_confirmatory(
observations : Array[@model.Observation],
baseline_id : String,
candidate_id : String,
dataset_id : Int,
) -> (Array[Double], Array[Double]) {
let baseline : Map[Int, Double] = Map([])
let candidate : Map[Int, Double] = Map([])
for observation in observations {
if observation.valid &&
observation.dataset_id == dataset_id &&
observation.phase is Confirmatory {
if observation.implementation_id == baseline_id {
baseline[observation.block_id] = observation.raw_elapsed_us
} else if observation.implementation_id == candidate_id {
candidate[observation.block_id] = observation.raw_elapsed_us
}
}
}
let left = []
let right = []
for block_id, value in baseline {
if candidate.get(block_id) is Some(other) {
left.push(value)
right.push(other)
}
}
(left, right)
}
fn sum_to(n : Int) -> Int {
let mut total = 0
for i in 0..<n {
total += i
}
total
}
async test "compare two implementations measured by the runner" {
let loop_sum = @runner.Implementation::stateless("loop", "1", (n : Int) => {
@model.OperationResult::completed(sum_to(n), ())
})
let formula = @runner.Implementation::stateless("formula", "1", (n : Int) => {
@model.OperationResult::completed(n * (n - 1) / 2, ())
})
let plan = @runner.single_step("triangle", [1000])
.with_immutable_input(context => context.dataset_key.scale, n => n.to_string())
.compare([loop_sum, formula])
.against_equal(n => n * (n - 1) / 2, (expected, actual) => expected == actual)
.compile()
.unwrap()
let memory = @event.InMemorySink::new()
let environment = @model.EnvironmentSnapshot::new(
@model.SemanticEnvironment::new(@model.ExecutionTarget::Native, "moonc", "", "i32"),
@model.PerformanceEnvironment::new("native", "laptop", "default", 1, "monotonic"),
@model.ProvenanceEnvironment::new("macos", "host", "now", "HEAD", "tutorial"),
)
let context = @runner.RunContext::new(
environment,
memory.as_sink(),
42UL,
@runner.ProtocolPreset::QuickCheck.validated(),
)
let summary = @runner.run(plan, context)
inspect(summary.passed_count, content="2")
let (baseline, candidate) = paired_confirmatory(
memory.observations, "loop", "formula", 0,
)
let result = @stats.compare_paired(
"loop", "formula", baseline, candidate, 5.0, @model.confirmatory_interval(),
)
inspect(result.valid_samples, content="3")
}
QuickCheck 运行三个验证性区组,因此有三对。决策本身取决于你的机器。
清理图表而不触动数据
把离群值策略应用于供图表使用的副本,而把原始数据保留给决策和事件流:
test "derived outlier views" {
let raw = [12.0, 12.5, 11.75, 12.0, 12.25, 31.5, 12.25, 12.0]
let view = @stats.filter_outliers(raw, @model.OutlierPolicy::TukeyFence)
inspect(view.length(), content="7")
inspect(raw.length(), content="8")
let everything = @stats.filter_outliers(raw, @model.OutlierPolicy::ReportOnly)
inspect(everything == raw, content="true")
}
更进一步
自行检查区间是否越过阈值。 决策只使用点估计。若要求整个区间都越过阈值,请把边界换算为基线中位数的百分比:
test "interval in percent" {
let baseline = [100.0, 102.0, 98.0, 101.0, 99.0, 103.0, 97.0, 100.0]
let candidate = [90.0, 93.0, 88.0, 92.0, 91.0, 94.0, 87.0, 90.0]
let result = @stats.compare_paired_with_bootstrap(
"baseline", "candidate", baseline, candidate, 2.0,
@model.confirmatory_interval(), 7UL, 2000, 95.0,
).unwrap()
let reference = @stats.summarize(baseline).median
let high_pct = result.interval.high / reference * 100.0
inspect(high_pct, content="-9")
inspect(high_pct <= -2.0, content="true")
}
即使是区间最不利的一端也有 9 % 的改进,因此结论并不取决于点估计。
每个数据集一个决策。 分别比较每个数据集(规模),并把各规模的标签交给 @experiment.crossover_from_labels,以找出胜者发生变化的规模;参见 experiment 教程。
由相对差值得到加速比。 speedup 与 relative_delta_pct 由 联系在一起;设计页面给出了推导。
常见陷阱
- 分别对数组排序。 配对是按位置进行的。分别对基线和候选排序会破坏配对;请对配对排序,而不是对数组排序。
- 混合不同阶段或目标。 探索性与验证性观测、native 与 JS 计时都属于不同的总体。请在配对之前进行过滤。
- 零阈值。 当
practical_delta_pct = 0.0时,恰好持平会被报告为Faster。 - 读取
summarize([])的结果。 空样本返回全零;请检查count。 - 直接比较区间和阈值。 区间以输入单位表示,阈值以百分比表示。
- 从源数据中删除离群值。 请过滤副本;JSONL 流是审计记录。