Skip to content

Design: benchmarking tooling

(For how to benchmark correctly, see ../benchmarking.md. This page records why the tooling and policy are designed the way they are.)

Why function-mode benchmark() warms up by default

The single most common error in Numba benchmarks found in the wild is timing the first call — which includes compilation — or timing from Python without warmup. The correct behavior must be the DEFAULT, not an option the user has to know to enable: benchmark(fn, args=...) runs uncounted warmup calls first, and including compilation requires the explicit warmup_runs=0. Defaults are the API's opinion; this one is the library's whole thesis.

Why benchmark is one hybrid function

with benchmark(): (block timing) and benchmark(fn) (function stats) could have been two names. One name won because they are one concept — "measure this" — and the argument type (callable vs label) makes the mode unambiguous. Two names would double the API for zero expressive gain, against the Minimal APIs principle.

Why baselines are the best standard tool, not a strawman

topk is measured against np.partition-based selection, not a full np.sort; containers are measured as jitted workload vs the idiomatic Python equivalent (heapq, set) — not as boxed per-call invocations that would flatter us. A benchmark answers "when does this library add value?", never "how do we win a chart?". When the honest baseline wins (radix_sort full-range vs NumPy's SIMD sort, counter vs np.unique), the row stays.

Why CI runs benchmarks but publishes no numbers

Shared CI runners have noisy, variable performance; publishing their numbers would launder noise into claims. CI's job is invariants — suites compile, run, and exit cleanly on every commit. Publishable numbers come from a controlled, documented environment: BENCHMARKS.md is regenerated locally with fixed seeds, fixed sizes, and the machine stated in its header.

Why results are frozen dataclasses

TimingStats, ComparisonResult, BenchmarkResult and FunctionReport are immutable. Measurement results are facts; letting code mutate a stats object after the fact is a category error, and frozen dataclasses make it a AttributeError instead of a silent one.