experiment 教程
本教程介绍基准测试的正确性工具:带容差的判定器、实现之间的关系检查、把失败缩小的缩减器,以及告诉你在哪个规模切换实现的交叉点分析。每个示例都是一个完整的测试。
快速上手
moon add Luna-Flow/mare_mark@0.3.0
import {
"Luna-Flow/mare_mark/model",
"Luna-Flow/mare_mark/experiment",
}
test "is the fast sum right?" {
let oracle = @experiment.ReferenceOracle::equal(
"exact-sum",
(xs : Array[Int]) => xs.fold(init=0, (a, b) => a + b),
(expected : Int, actual : Int) => expected == actual,
)
let input = [1, 2, 3, 4]
let verdict = @experiment.validate_reference(oracle, input, 0, Value(10), Value(10), "fast-sum", "4")
inspect(verdict.status is Valid, content="true")
}
日常任务
用容差比较浮点结果
重新结合后的求和与从左到右的求和在最后几位上有所不同。请接受一个相对误差,而不是要求相等:
test "relative tolerance" {
let close = (expected : Double, actual : Double) => {
(expected - actual).abs() <= 1.0e-12 * expected.abs().max(1.0)
}
let oracle = @experiment.ReferenceOracle::equal("sum", (xs : Array[Double]) => xs.fold(init=0.0, (a, b) => a + b), close)
let xs = [0.1, 0.2, 0.3]
let pairwise = xs[0] + (xs[1] + xs[2])
let verdict = @experiment.validate_reference(oracle, xs, 0, Value(0.1 + 0.2 + 0.3), Value(pairwise), "pairwise", "3")
inspect(verdict.status is Valid, content="true")
}
让实现相互检查
在没有可信参考的情况下,检查各实现是否一致:
test "implementations must agree" {
let agree : @experiment.RelationalOracle[Int, Int] = @experiment.RelationalOracle::new("agree", (_, _, _, left, _, right) => {
match (left, right) {
(Value(a), Value(b)) => if a == b { Valid } else { Invalid("results differ") }
_ => Invalid("missing result")
}
})
let spec : @experiment.OracleSpec[Int, Int, Int, Unit] = Relational(agree)
guard spec is Relational(oracle) else { fail("unexpected spec") }
inspect((oracle.validate_pair)(5, 0, "loop", Value(10), "formula", Value(10)) is Valid, content="true")
}
把 OracleSpec 传给 @runner.BenchSpec::advanced;运行器会检查每一对实现。
把失败缩小
某个列表函数对任何包含负数的列表都会失败。通过删除元素来缩减失败的输入:
test "shrink a list" {
let shrinker = @experiment.Shrinker::new(
(xs : Array[Int]) => Array::makei(xs.length(), i => {
let smaller = xs.copy()
smaller.remove(i) |> ignore
smaller
}),
xs => xs.length().to_string(),
)
let failing = [3, 8, -2, 7, 1]
let (minimal, _) = @experiment.shrink(failing, shrinker, xs => xs.any(x => x < 0))
debug_inspect(minimal, content="[-2]")
}
当 BenchSpec 带有 shrinker 时,运行器会调用同一个算法,并把最小输入写入失败事件。
找出切换实现的位置
在多个规模上比较两个实现,为每个规模打上标签,并寻找一次干净的转变:
test "crossover between two algorithms" {
let sizes = [8, 32, 128, 512, 2048]
let relative_delta_of_insertion_vs_merge = [-35.0, -12.0, 1.0, 18.0, 60.0]
let labels = relative_delta_of_insertion_vs_merge.map(r => @experiment.comparator_label(r, 5.0))
debug_inspect(labels, content="[\"A\", \"A\", \"Unknown\", \"B\", \"B\"]")
let domain = @experiment.ScaleDomain::new(sizes, (a, b) => a.compare(b), n => n.to_string())
let result = @experiment.crossover_from_labels(domain, labels)
inspect(result is NoCrossover(_, _), content="true")
let sharper = [-35.0, -12.0, -6.0, 18.0, 60.0].map(r => @experiment.comparator_label(r, 5.0))
guard @experiment.crossover_from_labels(domain, sharper) is Found(boundary, _, _) else { fail("none") }
inspect(boundary.at_or_above, content="512")
}
第一个序列在 A 区域和 B 区域之间有一个 Unknown;分析不会猜测分界位于这段间隙中的何处,而是报告没有交叉点。请更细致地测量这段间隙,或者自行决定策略。
更进一步
- 把
Found分界转化为@model.DeploymentPolicy::Piecewise;参见 model 教程。 - 用
ReferenceAndRelational组合参考判定器和关系判定器。 - experiment 设计证明了缩减会终止并保持失败。
常见陷阱
- 未排序的规模。
crossover_from_labels使用你给出的顺序。 - 对浮点结果使用相等比较。 请使用与算法误差界相匹配的容差。
- 会让输入变大的缩减器。 候选应当更小,否则预算会在毫无进展的情况下耗尽。
- 把
NonMonotonic当作交叉点。 它不是;偏好发生了翻转。
后续步骤
- experiment API、experiment 设计。
- runner 教程:运行中的判定器。