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]")
}

転置は実装ごとに 1 回、時計の外で行われます。

意図してセットアップを含める

「割り当てと計算」を 1 つのコストとして計測するには、反復ごとに準備し、それを計測に含めます:

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 の処理が計時されると思い込む。 計時されることはありません。
  • バッチごとのセットアップを含める。 そうすると償却コストがキャリブレーションされたバッチサイズに依存します。
  • 長寿命の値を破棄することでリセットする。 ランナーはリセット後にそれを再利用します。

次のステップ