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 工作会被计时。 它从不会被计时。
  • 计入按批次的准备工作。 这样摊销代价会取决于校准得到的批次大小。
  • 通过丢弃长期存活的值来重置。 运行器在重置后会复用它。

后续步骤