generator のチュートリアル

このチュートリアルでは、ベンチマークの入力を再現可能にします。1 つの実行シードからデータセットごとに独立したシードを作り、フィクスチャがそれを使って入力を生成し、フィンガープリントが何を計測したかを正確に記録します。どの例も完全なテストです。

クイックスタート

moon add Luna-Flow/mare_mark@0.3.0
import {
  "Luna-Flow/mare_mark/model",
  "Luna-Flow/mare_mark/generator",
  "Luna-Flow/mare_mark/fixture",
}
test "one run seed, many dataset seeds" {
  let run_seed = 2026UL
  let seeds = [0, 1, 2].map(id => @generator.derive_seed(run_seed, "dataset", id))
  inspect(seeds[0] != seeds[1] && seeds[1] != seeds[2], content="true")
  inspect(seeds[2] == @generator.derive_seed(2026UL, "dataset", 2), content="true")
}

日常的な作業

導出したシードからランダムな入力を生成する

導出したシードを使って任意の擬似乱数生成器を駆動します。この例では、他のパッケージを必要としないように小さな線形合同法生成器を使います:

fn random_array(seed : UInt64, length : Int) -> Array[Int] {
  let mut state = seed
  Array::makei(length, _ => {
    state = state * 6364136223846793005UL + 1442695040888963407UL
    (state >> 33).to_int() % 1000
  })
}

test "reproducible random input" {
  let seed = @generator.derive_seed(42UL, "sort-input", 0)
  let first = random_array(seed, 5)
  let again = random_array(seed, 5)
  inspect(first == again, content="true")
  inspect(first.length(), content="5")
}

フィクスチャ内で生成する

ランナーはフィクスチャに、実行シード、ケース ID、データセットキーを持つ GenerationContext を渡します。そこでデータセットのシードを導出します:

test "a seeded fixture" {
  let fixture : @fixture.Fixture[Int, Array[Int], Array[Int]] = @fixture.Fixture::immutable(
    "random-ints",
    "1",
    context => {
      let seed = @generator.derive_seed(context.seed, context.case_id, context.dataset_key.dataset_id)
      random_array(seed, context.dataset_key.scale)
    },
    xs => @generator.stable_fingerprint(xs.map(x => x.to_string()).join(",")),
  )
  let context = @generator.context(42UL, "suite", "sort", @model.DatasetKey::new(8, 0), fixture.id, fixture.version)
  let input = @fixture.materialize(fixture, context)
  inspect(input.length(), content="8")
  inspect(input == @fixture.materialize(fixture, context), content="true")
}

何を計測したかを記録する

入力の正規のシリアライズのフィンガープリントを取り、結果とともに保存します。ランナーは、フィクスチャのフィンガープリントをすべての検証イベントとすべての失敗アーティファクトに入れます:

test "fingerprints identify serializations" {
  let a = @generator.stable_fingerprint("[1,2,3]")
  let b = @generator.stable_fingerprint("[1, 2, 3]")
  inspect(a.length(), content="71")
  inspect(a == b, content="false")
}

2 つの文字列は同じ配列を表していますが、シリアライズは異なります。正規形を 1 つ選んでください。

計測ごとのシード

実験がブロックごとにノイズを引く場合は、3 つの ID すべてから導出します:

test "per-measurement seeds" {
  let seeds = [
    @generator.measurement_seed(1UL, 0, 0, 0),
    @generator.measurement_seed(1UL, 0, 0, 1),
    @generator.measurement_seed(1UL, 0, 1, 0),
    @generator.measurement_seed(1UL, 1, 0, 0),
  ]
  let distinct = seeds.all(s => seeds.filter(t => t == s).length() == 1)
  inspect(distinct, content="true")
}

さらに先へ

  • 生成関数とそのフィンガープリントを @generator.Generator に包み、値が変わったときは必ず version を上げてください。
  • generator の設計で、異なるインデックスや異なる実行シードが同じ導出シードを生まないことを証明しています。

よくある落とし穴

  • すべてのデータセットで context.seed を直接使う。 すべてのデータセットが同じストリームを引くことになります。データセットごとに導出してください。
  • 正規形でないテキストをハッシュする。 マップや浮動小数点数は、フィンガープリントを取る前に順序と形式を固定する必要があります。
  • フィンガープリントを署名として扱う。 フィンガープリントは鍵なしです。

次のステップ