Skip to content

← Under the Hood: Objects, Memory, Speed step 26 of 35

Medium Framework

timeit: take the minimum, and put the collector back

Two halves of an honest microbenchmark: the statistic you reduce with, and the interpreter state you measure under.

def reduce_ratio(timings_a: Sequence[float], timings_b: Sequence[float]) -> float: ...
def measure(*, repeat: int) -> dict[str, object]: ...

reduce_ratio returns min(timings_a) / min(timings_b). The starter uses statistics.mean, which is the instinct from statistics class and is the wrong instinct here. Timing noise is additive and one-sided — nothing makes code run faster than it can, and a context switch, an interrupt or another process makes it slower — so every sample is a true value plus non-negative noise and the minimum is the best estimator of the true value. The documentation is blunt about it: “the min() of the result is probably the only number you should be interested in.”

measure builds a timeit.Timer around the provided probe callable, calls autorange() to choose a loop count, then repeat(repeat=repeat, number=number), and reports four facts:

key meaning
"samples" len(timings) — must equal repeat
"loops_ok" the loop count autorange chose is at least 1
"gc_on_during" whether the garbage collector was enabled while the timed statement ran
"gc_enabled_after" whether it is enabled once measure returns

probe records gc.isenabled() into a module dict every time it runs, so gc_on_during is an observation rather than an assertion about timing — which is the point. Timer.timeit calls gc.disable() before running the timing function, so with a bare timeit.Timer(probe) the answer is False. The documented workaround is to re-enable it in the setup, which runs inside the timing function after the disable:

timeit.Timer(probe, setup="import gc; gc.enable()")

That matters whenever the thing you are measuring allocates — a parser, a serialiser, an ORM materialisation. Collection is part of the cost of allocation, and benchmarking with it off measures a program you do not deploy.

solve(timings_a, timings_b, do_measure, repeat) returns {"ratio": <rounded to 9 dp>, "measured": <the dict above, or None>}.

Your submission must pass mypy --strict.