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 は で結び付いています。設計ページでこれを導いています。
よくある落とし穴
- 配列を個別にソートする。 ペアの対応は位置によります。ベースラインと候補を別々にソートするとペアの対応が壊れます。配列ではなくペアをソートしてください。
- フェーズやターゲットを混在させる。 探索的な観測と確認的な観測、native と JS の計時値は、それぞれ異なる母集団です。ペアにする前にフィルタリングしてください。
- ゼロの閾値。
practical_delta_pct = 0.0では、完全な同値がFasterと報告されます。 summarize([])を読む。 空の標本はゼロを返します。countを確認してください。- 区間と閾値を直接比較する。 区間は入力の単位で、閾値はパーセントです。
- 元データから外れ値を削除する。 コピーをフィルタリングしてください。JSONL ストリームは監査記録です。
次のステップ
- すべての関数とエラーについては stats API。
- 推定量、ブートストラップ、閾値の規則については stats の設計。
- ここで比較する観測を生成するには runner のチュートリアル。
- 結果を公開するには report のチュートリアル。