stats のチュートリアル

このチュートリアルでは、計時値を擁護できる判断に変えます。頑健な要約、実用的な閾値に対する対応のある比較、再現可能なブートストラップ区間、そして生のデータには手を付けないプロット用の整理されたビューです。例は完全なテストです。以下に示すモジュールをインポートするパッケージに貼り付けて、moon test を実行してください。

クイックスタート

モジュールを追加し、2 つのパッケージをインポートします:

moon add Luna-Flow/mare_mark@0.3.0
import {
  "Luna-Flow/mare_mark/model",
  "Luna-Flow/mare_mark/stats",
  "Luna-Flow/mare_mark/event",
  "Luna-Flow/mare_mark/runner",
  "moonbitlang/async",
}

同じ 8 個のブロックで計測した候補とベースラインを比較します:

test "is the candidate at least 2 % faster?" {
  let baseline = [100.0, 102.0, 98.0, 101.0, 99.0, 103.0, 97.0, 100.0]
  let candidate = [90.0, 93.0, 88.0, 92.0, 91.0, 94.0, 87.0, 90.0]
  let result = @stats.compare_paired(
    "baseline", "candidate", baseline, candidate, 2.0, @model.confirmatory_interval(),
  )
  inspect(result.relative_delta_pct, content="-9.5")
  inspect(@stats.is_faster(result), content="true")
}

ペアごとの差の中央値はベースラインの中央値の 9.5 % で、2 % の閾値を大きく超えているため、判断は Faster です。

日常的な作業

1 つの実装を要約する

summarize は古典的な統計量と頑健な統計量を並べて示します。平均と中央値が食い違う場合、その標本には調べる価値のある裾があります。

test "summarize timings" {
  let timings = [12.0, 12.5, 11.75, 12.0, 12.25, 31.5, 12.25, 12.0]
  let summary = @stats.summarize(timings)
  inspect(summary.median, content="12.125")
  inspect(summary.mean, content="14.53125")
  inspect(summary.iqr, content="0.3125")
  inspect(summary.max, content="31.5")
}

31.5 µs の 1 回の実行が平均を 14.5 µs に引き上げていますが、中央値は 12.1 µs のままです。中央値と IQR を報告し、最大値は診断用に残してください。

実用的な閾値で判断する

閾値は、気にかける最小の変化を表します。1 % の改善は、0.5 % の閾値では Faster、2 % の閾値では Equivalent です:

test "the threshold decides what counts" {
  let baseline = [100.0, 100.0, 100.0, 100.0]
  let candidate = [99.0, 99.0, 99.0, 99.0]
  let strict = @stats.compare_paired(
    "a", "b", baseline, candidate, 0.5, @model.confirmatory_interval(),
  )
  let lenient = @stats.compare_paired(
    "a", "b", baseline, candidate, 2.0, @model.confirmatory_interval(),
  )
  inspect(strict.decision is Faster, content="true")
  inspect(lenient.decision is Equivalent, content="true")
}

閾値はデータを見る前に選び、結果とともに記録してください。

再現可能な区間を追加する

compare_paired_with_bootstrap は判断をそのまま保ち、四分位区間を中央値の差に対するパーセンタイル・ブートストラップ区間に置き換えます。レポートを再生成しても同じ数値が得られるよう、シードと再標本化の回数を固定してください:

test "bootstrap interval of the median delta" {
  let baseline = [100.0, 102.0, 98.0, 101.0, 99.0, 103.0, 97.0, 100.0]
  let candidate = [90.0, 93.0, 88.0, 92.0, 91.0, 94.0, 87.0, 90.0]
  let first = @stats.compare_paired_with_bootstrap(
    "baseline", "candidate", baseline, candidate, 2.0,
    @model.confirmatory_interval(), 2026UL, 2000, 95.0,
  ).unwrap()
  let again = @stats.compare_paired_with_bootstrap(
    "baseline", "candidate", baseline, candidate, 2.0,
    @model.confirmatory_interval(), 2026UL, 2000, 95.0,
  ).unwrap()
  inspect(first.interval.low, content="-10")
  inspect(first.interval.high, content="-9")
  inspect(first.interval.low == again.interval.low, content="true")
}

区間の単位は入力の単位である µs です。中央値の差は、95 % のブートストラップ信頼度で −10 µs から −9 µs の間にあります。

実行の観測をペアにする

ランナーは実装とブロックごとに 1 つの Observation を出力します。それらを block_id でペアにし、有効な確認的観測だけを残して比較します。この例は実際のベンチマークを実行するため、マシンに依存しない数値だけを示します:

fn paired_confirmatory(
  observations : Array[@model.Observation],
  baseline_id : String,
  candidate_id : String,
  dataset_id : Int,
) -> (Array[Double], Array[Double]) {
  let baseline : Map[Int, Double] = Map([])
  let candidate : Map[Int, Double] = Map([])
  for observation in observations {
    if observation.valid &&
      observation.dataset_id == dataset_id &&
      observation.phase is Confirmatory {
      if observation.implementation_id == baseline_id {
        baseline[observation.block_id] = observation.raw_elapsed_us
      } else if observation.implementation_id == candidate_id {
        candidate[observation.block_id] = observation.raw_elapsed_us
      }
    }
  }
  let left = []
  let right = []
  for block_id, value in baseline {
    if candidate.get(block_id) is Some(other) {
      left.push(value)
      right.push(other)
    }
  }
  (left, right)
}

fn sum_to(n : Int) -> Int {
  let mut total = 0
  for i in 0..<n {
    total += i
  }
  total
}

async test "compare two implementations measured by the runner" {
  let loop_sum = @runner.Implementation::stateless("loop", "1", (n : Int) => {
    @model.OperationResult::completed(sum_to(n), ())
  })
  let formula = @runner.Implementation::stateless("formula", "1", (n : Int) => {
    @model.OperationResult::completed(n * (n - 1) / 2, ())
  })
  let plan = @runner.single_step("triangle", [1000])
    .with_immutable_input(context => context.dataset_key.scale, n => n.to_string())
    .compare([loop_sum, formula])
    .against_equal(n => n * (n - 1) / 2, (expected, actual) => expected == actual)
    .compile()
    .unwrap()
  let memory = @event.InMemorySink::new()
  let environment = @model.EnvironmentSnapshot::new(
    @model.SemanticEnvironment::new(@model.ExecutionTarget::Native, "moonc", "", "i32"),
    @model.PerformanceEnvironment::new("native", "laptop", "default", 1, "monotonic"),
    @model.ProvenanceEnvironment::new("macos", "host", "now", "HEAD", "tutorial"),
  )
  let context = @runner.RunContext::new(
    environment,
    memory.as_sink(),
    42UL,
    @runner.ProtocolPreset::QuickCheck.validated(),
  )
  let summary = @runner.run(plan, context)
  inspect(summary.passed_count, content="2")
  let (baseline, candidate) = paired_confirmatory(
    memory.observations, "loop", "formula", 0,
  )
  let result = @stats.compare_paired(
    "loop", "formula", baseline, candidate, 5.0, @model.confirmatory_interval(),
  )
  inspect(result.valid_samples, content="3")
}

QuickCheck は 3 つの確認的ブロックを実行するため、ペアは 3 つです。判断そのものはマシンに依存します。

データに触れずにプロットを整理する

外れ値ポリシーは、プロットに渡すコピーに適用し、元のデータは判断とイベントストリームのために保持します:

test "derived outlier views" {
  let raw = [12.0, 12.5, 11.75, 12.0, 12.25, 31.5, 12.25, 12.0]
  let view = @stats.filter_outliers(raw, @model.OutlierPolicy::TukeyFence)
  inspect(view.length(), content="7")
  inspect(raw.length(), content="8")
  let everything = @stats.filter_outliers(raw, @model.OutlierPolicy::ReportOnly)
  inspect(everything == raw, content="true")
}

さらに先へ

区間を閾値と自分で照合する。 判断は点推定値だけを使います。区間全体が閾値を超えることを要求するには、上下限をベースラインの中央値に対するパーセントに変換します:

test "interval in percent" {
  let baseline = [100.0, 102.0, 98.0, 101.0, 99.0, 103.0, 97.0, 100.0]
  let candidate = [90.0, 93.0, 88.0, 92.0, 91.0, 94.0, 87.0, 90.0]
  let result = @stats.compare_paired_with_bootstrap(
    "baseline", "candidate", baseline, candidate, 2.0,
    @model.confirmatory_interval(), 7UL, 2000, 95.0,
  ).unwrap()
  let reference = @stats.summarize(baseline).median
  let high_pct = result.interval.high / reference * 100.0
  inspect(high_pct, content="-9")
  inspect(high_pct <= -2.0, content="true")
}

区間の最も不利な端でも 9 % の改善なので、結論は点推定値に左右されません。

データセットごとに 1 つの判断。 各データセット(スケール)を個別に比較し、スケールごとのラベルを @experiment.crossover_from_labels に渡して、勝者が切り替わるスケールを見つけます。experiment のチュートリアルを参照してください。

相対差からの高速化率。 speedup と relative_delta_pct は s=1/(1+r/100)s = 1/(1 + r/100) で結び付いています。設計ページでこれを導いています。

よくある落とし穴

  • 配列を個別にソートする。 ペアの対応は位置によります。ベースラインと候補を別々にソートするとペアの対応が壊れます。配列ではなくペアをソートしてください。
  • フェーズやターゲットを混在させる。 探索的な観測と確認的な観測、native と JS の計時値は、それぞれ異なる母集団です。ペアにする前にフィルタリングしてください。
  • ゼロの閾値。 practical_delta_pct = 0.0 では、完全な同値が Faster と報告されます。
  • summarize([]) を読む。 空の標本はゼロを返します。count を確認してください。
  • 区間と閾値を直接比較する。 区間は入力の単位で、閾値はパーセントです。
  • 元データから外れ値を削除する。 コピーをフィルタリングしてください。JSONL ストリームは監査記録です。

次のステップ