fixture 教程
本教程展示如何把基准测试输入安全地交给各个实现:所有人共享的不可变输入、每个批次之前复制的可变输入、针对每个实现以不同方式准备的输入,以及有意计入测量的准备代价。每个示例都是一个完整的测试。
快速上手
moon add Luna-Flow/mare_mark@0.3.0
import {
"Luna-Flow/mare_mark/model",
"Luna-Flow/mare_mark/fixture",
}
test "an immutable input" {
let fixture = @fixture.Fixture::immutable(
"squares", "1", context => Array::makei(context.dataset_key.scale, i => i * i), (xs : Array[Int]) => xs.length().to_string(),
)
let context = @model.GenerationContext::new(7UL, "suite", "sum", @model.DatasetKey::new(4, 0), fixture.id, fixture.version)
debug_inspect(@fixture.materialize(fixture, context), content="[0, 1, 4, 9]")
}
日常任务
复制可变输入
原地排序的实现每次都必须拿到一个新的副本:
test "clone before prepare" {
let fixture : @fixture.Fixture[Int, Array[Int], Array[Int]] = @fixture.Fixture::new(
"reversed", "1",
context => Array::makei(context.dataset_key.scale, i => context.dataset_key.scale - i),
xs => xs.length().to_string(),
xs => xs.copy(),
(xs, _, _) => xs,
(_, _) => (),
@model.SetupPolicy::new(@model.SetupFrequency::PerBatch, @model.SetupTiming::ExcludedFromMeasurement, @model.WorkspaceScope::BatchWorkspace),
)
let context = @model.GenerationContext::new(0UL, "s", "sort", @model.DatasetKey::new(3, 0), fixture.id, fixture.version)
let input = @fixture.materialize(fixture, context)
let prepared = @fixture.prepare(fixture, (fixture.clone_input)(input), "sort", @fixture.SampleContext::new(0, "sort", 0))
prepared.sort()
debug_inspect(prepared, content="[1, 2, 3]")
debug_inspect(input, content="[3, 2, 1]")
}
运行器会替你调用 clone_input;这里的显式调用展示了实际发生的过程。
按实现准备
prepare 会收到实现 id。为每个实现提供它所期望的布局:
test "layout per implementation" {
let fixture : @fixture.Fixture[Int, Array[Int], Array[Int]] = @fixture.Fixture::new(
"matrix-2x2", "1",
_ => [1, 2, 3, 4],
xs => xs.length().to_string(),
xs => xs.copy(),
(xs, implementation, _) => if implementation == "column-major" { [xs[0], xs[2], xs[1], xs[3]] } else { xs },
(_, _) => (),
@model.SetupPolicy::new(@model.SetupFrequency::PerImplementation, @model.SetupTiming::ExcludedFromMeasurement, @model.WorkspaceScope::ImplementationWorkspace),
)
let sample = @fixture.SampleContext::new(0, "column-major", 0)
debug_inspect(@fixture.prepare(fixture, [1, 2, 3, 4], "column-major", sample), content="[1, 3, 2, 4]")
}
转置对每个实现只发生一次,并且在时钟之外。
有意计入准备工作
要把“分配并计算”作为一个整体代价来测量,请按迭代准备并将其计入:
test "setup inside the measurement" {
let policy = @model.SetupPolicy::new(
@model.SetupFrequency::PerIteration,
@model.SetupTiming::IncludedInMeasurement,
@model.WorkspaceScope::OperationWorkspace,
)
let fixture : @fixture.Fixture[Int, Int, Array[Double]] = @fixture.Fixture::new(
"fresh-buffer", "1",
context => context.dataset_key.scale,
n => n.to_string(),
n => n,
(n, _, _) => Array::make(n, 0.0),
(_, _) => (),
policy,
)
inspect(fixture.setup_policy.timing is IncludedInMeasurement, content="true")
}
这样每个观测都会把 setup_timing 记录为 IncludedInMeasurement,读者就知道这个数字包含了分配。
更进一步
- 用
SampleContext的sample_id区分验证(-1)、预热和校准(其他负 id)与被测量的区组;runner 设计列出了它们。 - 用
@generator.derive_seed从context.seed派生随机输入;参见 generator 教程。
常见陷阱
- 对会修改输入的实现使用恒等
clone_input。 之后的批次会看到被修改过的数据。 - 以为昂贵的
materialize工作会被计时。 它从不会被计时。 - 计入按批次的准备工作。 这样摊销代价会取决于校准得到的批次大小。
- 通过丢弃长期存活的值来重置。 运行器在重置后会复用它。
后续步骤
- fixture API、fixture 设计。
- runner 教程:用这些夹具运行用例。