The tests package benchmarks run at very low iteration counts, so the reported ns/op is coarsely quantised and moves when b.N moves. At perf-pr.yml's -benchtime 500ms the worst of them runs one iteration.
Measured on a dedicated c7a.2xlarge (AMD EPYC 9R14, go1.26.0), 16 commits × 5 rounds at -count 3 -benchtime 1s — the perf-timeline.yml settings:
| benchmark |
b.N observed |
BenchmarkArith |
4–8 |
BenchmarkAdd |
2–5 |
BenchmarkSetIndex |
2 |
BenchmarkFibPlaceholderRun |
1–2 |
BenchmarkMatrixMult |
150–260 (fine) |
perf-pr.yml uses -benchtime 500ms, so those counts roughly halve again.
Why it matters beyond coarseness
ns/op here is not simply duration / N with a constant per-op cost. The benchmarks create one interpreter outside the loop and reuse it:
func BenchmarkArith(b *testing.B) {
chunk := compileFile(b, "scripts/bench_arith.ts")
paserati := driver.NewPaserati()
...
for i := 0; i < b.N; i++ {
_, runtimeErrs := paserati.InterpretChunk(chunk)
}
}
So per-iteration cost falls as caches and IC state warm, and the amortised ns/op is genuinely N-dependent. Going from N=5 to N=8 changes the number without anything in the engine changing.
In our corpus this was visible as: commits where b.N moved had a median round-to-round spread of 32.0%, against 0.8% for commits where it held. Same source, same machine, same session.
What this is not
I initially thought this caused an instability we were chasing. It does not — pinning the count with -benchtime Nx and relaunching 20 times changed little, and made one control case worse. b.N is an output of the timing loop (N ≈ benchtime / per-op cost), so it moves with the measurement rather than driving it.
The claim here is narrower and I think solid: a metric quantised to 1-in-8 (or 1-in-1) cannot honestly report the 1–5% changes the perf lanes exist to detect.
Suggested fix
Raise -benchtime for the tests package until b.N is comfortably above ~20, or pin it per benchmark with Nx. MatrixMult already sits in a healthy range, so this is mostly about the four cheap-script benchmarks.
Costs wall clock. Buys a number whose resolution matches what it is being asked to resolve.
Provenance
Full data, the eight diagnostic experiments, and a corrections index (this investigation produced three wrong explanations before this one) are in my notes rather than upstream; happy to attach raw samples[] output if useful. iterations is already recorded in every sample, so this is checkable against any existing snapshot without re-running anything.
Tracker: #23. Adjacent but independent of #22, which scopes the reducer rather than the sampling underneath it.
The
testspackage benchmarks run at very low iteration counts, so the reportedns/opis coarsely quantised and moves whenb.Nmoves. Atperf-pr.yml's-benchtime 500msthe worst of them runs one iteration.Measured on a dedicated
c7a.2xlarge(AMD EPYC 9R14, go1.26.0), 16 commits × 5 rounds at-count 3 -benchtime 1s— theperf-timeline.ymlsettings:b.NobservedBenchmarkArithBenchmarkAddBenchmarkSetIndexBenchmarkFibPlaceholderRunBenchmarkMatrixMultperf-pr.ymluses-benchtime 500ms, so those counts roughly halve again.Why it matters beyond coarseness
ns/ophere is not simplyduration / Nwith a constant per-op cost. The benchmarks create one interpreter outside the loop and reuse it:So per-iteration cost falls as caches and IC state warm, and the amortised
ns/opis genuinely N-dependent. Going from N=5 to N=8 changes the number without anything in the engine changing.In our corpus this was visible as: commits where
b.Nmoved had a median round-to-round spread of 32.0%, against 0.8% for commits where it held. Same source, same machine, same session.What this is not
I initially thought this caused an instability we were chasing. It does not — pinning the count with
-benchtime Nxand relaunching 20 times changed little, and made one control case worse.b.Nis an output of the timing loop (N ≈ benchtime / per-op cost), so it moves with the measurement rather than driving it.The claim here is narrower and I think solid: a metric quantised to 1-in-8 (or 1-in-1) cannot honestly report the 1–5% changes the perf lanes exist to detect.
Suggested fix
Raise
-benchtimefor thetestspackage untilb.Nis comfortably above ~20, or pin it per benchmark withNx.MatrixMultalready sits in a healthy range, so this is mostly about the four cheap-script benchmarks.Costs wall clock. Buys a number whose resolution matches what it is being asked to resolve.
Provenance
Full data, the eight diagnostic experiments, and a corrections index (this investigation produced three wrong explanations before this one) are in my notes rather than upstream; happy to attach raw
samples[]output if useful.iterationsis already recorded in every sample, so this is checkable against any existing snapshot without re-running anything.Tracker: #23. Adjacent but independent of #22, which scopes the reducer rather than the sampling underneath it.