Skip to content
About
Main content

Measure your noise floor and your winner's curse (45 minutes)

Measure your machineNeeds Python with NumPy

Problem

Before a benchmark can show that a change helped, it has to show what "no change" looks like. Repeated identical runs differ by a few percent (the ), so smaller "improvements" are inside the noise. Picking the best of many noisy configurations finds the luckiest run, which regresses to the mean when measured again. And when conditions drift during a test (a background job starts, the machine heats up), running all of A and then all of B turns the drift into a fake difference; A and B cancels it.

The benchmark sorts 2 million random integers. (1) Run the identical command 10 times and report the A/A spread as (max − min) / mean. (2) Make a "tuning sweep": 30 identical configurations (a dummy parameter), run each once, pick the best, and re-run the "winner" 5 more times. (3) Compare A with B (identical code), once A-then-B in blocks and once interleaved, while CPU-heavy processes start halfway through.

The question. How large is the noise floor, how much does a sweep winner keep on re-measurement, and what does run order do under drifting load?


Predict

With A and B identical, run in blocked order while background load drifts, how much slower does B look?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    measure-your-noise-floor-and-your.py68 linesDownload
    View the code
    measure-your-noise-floor-and-your.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["numpy"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Measure your noise floor and your tuning sweep.
    
    The benchmark sorts 2 million random integers (NumPy). (1) The identical run
    10 times: the A/A spread. (2) A "tuning sweep" of 30 identical configurations
    (a dummy parameter), best picked, then re-measured 5 times. (3) A against B
    (identical code) in blocks and interleaved, with a CPU-heavy background load
    started halfway through.
    """
    import argparse, json, multiprocessing as mp, statistics, time
    import numpy as np
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--size", type=int, default=2_000_000, help="Integers to sort per run.")
    p.add_argument("--reps", type=int, default=5, help="Sorts per run (the run reports their median).")
    p.add_argument("--sweep", type=int, default=30, help="Configurations in the dummy sweep.")
    p.add_argument("--pairs", type=int, default=20, help="A/B runs of each in the order test.")
    p.add_argument("--load-procs", type=int, default=8, help="Background busy processes in the order test.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    data = np.random.default_rng(7).integers(0, 1 << 40, args.size)
    def run(_cfg=None):
        ts = []
        for _ in range(args.reps):
            a = data.copy(); t = time.perf_counter(); a.sort(); ts.append(time.perf_counter() - t)
        return statistics.median(ts) * 1000
    
    def burn(stop):
        x = 0
        while not stop.is_set(): x += 1
    
    run()                                                                      # warm up
    aa = [run() for _ in range(10)]
    spread = (max(aa) - min(aa)) / statistics.mean(aa)
    
    sweep = [{"config": i, "ms": run(i)} for i in range(args.sweep)]
    best = min(sweep, key=lambda r: r["ms"]); rerun = [run(best["config"]) for _ in range(5)]
    sweep_mean = statistics.mean(r["ms"] for r in sweep)
    
    def order_test(interleaved):
        stop = mp.Event(); procs = []; a, b = [], []
        seq = ["A", "B"] * args.pairs if interleaved else ["A"] * args.pairs + ["B"] * args.pairs
        for i, who in enumerate(seq):
            if i == len(seq) // 2:                                             # background load starts halfway through
                procs = [mp.Process(target=burn, args=(stop,)) for _ in range(args.load_procs)]; [q.start() for q in procs]
            (a if who == "A" else b).append(run())
        stop.set(); [q.join() for q in procs]
        return (statistics.mean(b) - statistics.mean(a)) / statistics.mean(a)
    
    if __name__ == "__main__":
        blocked, inter = order_test(False), order_test(True)
        summary = {"aa_spread": spread, "aa_min": min(aa), "aa_max": max(aa), "sweep_best": best["ms"], "sweep_mean": sweep_mean,
                   "best_gain": (sweep_mean - best["ms"]) / sweep_mean, "rerun_mean": statistics.mean(rerun),
                   "rerun_gain": (sweep_mean - statistics.mean(rerun)) / sweep_mean, "blocked_diff": blocked, "interleaved_diff": inter}
        summary.update({k + "_pct": v * 100 for k, v in list(summary.items()) if k in ("aa_spread", "best_gain", "rerun_gain", "blocked_diff", "interleaved_diff")})
        print(f"A/A: 10 identical runs {min(aa):.1f}-{max(aa):.1f} ms, spread {spread:.1%}")
        print(f"sweep: best of {args.sweep} identical configs {best['ms']:.1f} ms ({summary['best_gain']:.1%} better than their mean); re-measured {summary['rerun_mean']:.1f} ms ({summary['rerun_gain']:.1%})")
        print(f"identical A and B under a load that starts halfway: blocked order says B is {blocked:+.1%}, interleaved says {inter:+.1%}")
        if args.json:
            with open(args.json, "w") as f:
                json.dump({"summary": summary, "tables": {"aa": [{"run": i + 1, "ms": v} for i, v in enumerate(aa)], "sweep": sweep}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run measure-your-noise-floor-and-your.py
    uv run measure-your-noise-floor-and-your.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--size2,000,000 Integers to sort per run.
--reps5 Sorts per run (the run reports their median).
--sweep30 Configurations in the dummy sweep.
--pairs20 A/B runs of each in the order test.
--load-procs8 Background busy processes in the order test.

Our run

Thirty identical "configurations"Apple M2 · 8 cores · 8 GB
Data table
Thirty identical "configurations"
ConfigurationMedian sort time (ms)
055.3
155.9
257.3
355.9
455.7
556.4
657.4
756.6
856.1
956.2
1055.2
1155.4
1255.4
1355.7
1456.2
1555.3
1655.6
1755.1
1855.4
1955.4
2056.3
2154.9
2256.1
2355.5
2455.1
2555.1
2655.1
2757.1
2856.8
2956.9
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20
Output · uv run measure-your-noise-floor-and-your.py
A/A: 10 identical runs 55.1-57.4 ms, spread 4.2%
sweep: best of 30 identical configs 54.9 ms (1.7% better than their mean); re-measured 56.0 ms (-0.2%)
identical A and B under a load that starts halfway: blocked order says B is +58.2%, interleaved says -1.2%

What we found

Ten identical runs ranged from 55.1 to 57.4 ms, a spread of 4.17% of the mean. The best of 30 identical configurations looked 1.71% better than their mean; re-measured, its advantage was -0.228%: regression to the mean. With A and B identical, the blocked order reported B as 58.2% slower, the background load's effect; interleaved, -1.16%.


Read it

The noise floor is the first measurement of any benchmark, and it belongs next to every result. Treat a sweep's winner as a candidate to re-measure, not a result. And interleave whatever you compare, so that anything that drifts during the run affects both sides equally.