stats 教程

本教程把计时转化为站得住脚的决策:稳健的摘要、基于实际阈值的成对比较、可复现的自助法区间,以及不触动原始数据的、用于图表的清理后视图。示例都是完整的测试;把其中一个粘贴到导入了下面所示模块的包中,然后运行 moon test。

快速上手

添加模块并导入这两个包:

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",
}

把候选与在相同八个区组中测量的基线进行比较:

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。

日常任务

总结一个实现

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 的运行把均值拉到 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 之间。

对一次运行中的观测进行配对

运行器为每个实现和区组发出一个 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 运行三个验证性区组,因此有三对。决策本身取决于你的机器。

清理图表而不触动数据

把离群值策略应用于供图表使用的副本,而把原始数据保留给决策和事件流:

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 % 的改进,因此结论并不取决于点估计。

每个数据集一个决策。 分别比较每个数据集(规模),并把各规模的标签交给 @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 流是审计记录。

后续步骤