You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every tin hand-rolls its benchmarks: perf_counter_ns around a loop, best-of-N, divide by hand for MB/s. That was fine when there was one tin. It is now thirteen, the numbers are quoted on magmalake.org and in every README, and the method has known weaknesses. Mojo ships std.benchmark, which fixes most of them.
What the hand-rolled harness gets wrong
Nothing stops the optimiser eliding the work.hashes, snappy and roaring time pure functions whose results are often discarded. We currently avoid dead-code elimination by accident — a stray print or an accumulator that happens to escape — not by construction. std.benchmark.compiler.keep exists precisely for this:
There is also std.benchmark.memory.clobber_memory() for the store-elision case.
Iteration counts are guesses. Each bench picks a fixed N. run() chooses batches adaptively:
benchmarking continues until min_runtime_secs has elapsed and either max_runtime_secs or max_iters is achieved
Best-of-N is not a statistic. Several READMEs carry caveats like "wide run-to-run variance on this shared dev machine". Report exposes mean, fastest/slowest batch mean, iteration count and warmup total — enough to report a number with a spread instead of an apology.
Throughput is computed by hand. Every repo divides bytes by seconds itself. BenchMetric has the units built in:
metric
unit
BenchMetric.elements
GElems/s
BenchMetric.bytes
GB/s
BenchMetric.flops
GFLOPS/s
Results are prose, not data. Numbers are pasted into READMEs by hand and go stale — the org README drifted from the site for exactly this reason. Bench writes Format.csv, Format.tabular or Format.table, which a script can diff between commits.
Proposed shape
Two tiers, because the tins are not all the same kind of thing.
Tier 1 — microbenchmarks (hashes, snappy, roaring, thrift, parts of avro): use std.benchmark.run with keep, and ThroughputMeasure(BenchMetric.bytes, n) for the rate. These are tight loops over a buffer, exactly what the module is designed for.
Tier 2 — whole-file / whole-scan (parquet, iceberg, objectstore): the unit of work is milliseconds and involves IO, so adaptive iteration matters less and setup cost dominates. Use Bench + bench_with_input where it fits; keep an explicit timed region where it does not. Do not force tier 1 onto tier 2 — a 4 ms Parquet read does not want 100 warmup iterations against a cold page cache.
Where a number is IO-bound or otherwise not a fair run() target, say so in the bench output rather than quietly reporting a shape the harness cannot measure honestly.
Assess tier 2 for parquet, iceberg, objectstore — report what Bench can and cannot express for them before converting anything
Emit Format.csv alongside human output so numbers can be diffed across commits
Re-measure and update the README and magmalake.org tables from the new harness; expect some published numbers to move, and treat that as the point rather than something to paper over
Decide whether benches run in CI (guarding against regressions) or stay manual (they are slow, and shared runners are noisy)
Non-goals
Not chasing faster numbers. This is about making the existing ones trustworthy and reproducible. If a number gets worse because keep stopped the optimiser deleting the work, that is the harness finding a real bug in the measurement — publish the corrected figure.
Every tin hand-rolls its benchmarks:
perf_counter_nsaround a loop, best-of-N, divide by hand for MB/s. That was fine when there was one tin. It is now thirteen, the numbers are quoted on magmalake.org and in every README, and the method has known weaknesses. Mojo shipsstd.benchmark, which fixes most of them.What the hand-rolled harness gets wrong
Nothing stops the optimiser eliding the work.
hashes,snappyandroaringtime pure functions whose results are often discarded. We currently avoid dead-code elimination by accident — a strayprintor an accumulator that happens to escape — not by construction.std.benchmark.compiler.keepexists precisely for this:There is also
std.benchmark.memory.clobber_memory()for the store-elision case.Iteration counts are guesses. Each bench picks a fixed N.
run()chooses batches adaptively:Best-of-N is not a statistic. Several READMEs carry caveats like "wide run-to-run variance on this shared dev machine".
Reportexposes mean, fastest/slowest batch mean, iteration count and warmup total — enough to report a number with a spread instead of an apology.Throughput is computed by hand. Every repo divides bytes by seconds itself.
BenchMetrichas the units built in:BenchMetric.elementsGElems/sBenchMetric.bytesGB/sBenchMetric.flopsGFLOPS/sResults are prose, not data. Numbers are pasted into READMEs by hand and go stale — the org README drifted from the site for exactly this reason.
BenchwritesFormat.csv,Format.tabularorFormat.table, which a script can diff between commits.Proposed shape
Two tiers, because the tins are not all the same kind of thing.
Tier 1 — microbenchmarks (
hashes,snappy,roaring,thrift, parts ofavro): usestd.benchmark.runwithkeep, andThroughputMeasure(BenchMetric.bytes, n)for the rate. These are tight loops over a buffer, exactly what the module is designed for.Tier 2 — whole-file / whole-scan (
parquet,iceberg,objectstore): the unit of work is milliseconds and involves IO, so adaptive iteration matters less and setup cost dominates. UseBench+bench_with_inputwhere it fits; keep an explicit timed region where it does not. Do not force tier 1 onto tier 2 — a 4 ms Parquet read does not want 100 warmup iterations against a cold page cache.Where a number is IO-bound or otherwise not a fair
run()target, say so in the bench output rather than quietly reporting a shape the harness cannot measure honestly.Scope
hashes.mojo— three pure functions, the clearest case, and it proves the pattern includingkeep(bench: move to the shared bench.mojo harness hashes.mojo#1; stable-only, see the comment below)snappy,roaring,thrift,avroparquet,iceberg,objectstore— report whatBenchcan and cannot express for them before converting anythingFormat.csvalongside human output so numbers can be diffed across commitsNon-goals
Not chasing faster numbers. This is about making the existing ones trustworthy and reproducible. If a number gets worse because
keepstopped the optimiser deleting the work, that is the harness finding a real bug in the measurement — publish the corrected figure.