Skip to content
About
Main content

A micro-benchmark that disappears, and a sampling blind spot

Measure your machineNeeds C compiler, Python

Problem

Two blind spots of measurement. A whose result is never used can be removed by the compiler, so it measures nothing and reports an absurdly good time. A profiler sees an event only if a sample lands on it: a 2 ms event once a second is shorter than the 10 ms between samples at 99 Hz, so most occurrences are never sampled, and it is visible only statistically, over many samples.

Part A: a loop that computes x * x for 1 billion values and never uses the result, compiled with -O2; then the same loop with the result summed and printed. Part B: a program that runs a 2 ms sleep about once a second and does busy work otherwise, profiled for 60 seconds at 99 Hz and then 999 Hz (by a timer-signal sampler in the same process). Count how many samples fall in the sleep and how many of the 60 sleeps are seen at all, and the profiler's overhead on the busy work.

The question. What does the unused loop measure, and how much of the short sleep does each sampling rate see?


Predict

Sampling at 99 Hz for a minute, how many of the 2 ms sleeps does the profiler see at all?

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.

    a-micro-benchmark-that-disappears-and.py79 linesDownload
    View the code
    a-micro-benchmark-that-disappears-and.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """A micro-benchmark that disappears, and a profiler's blind spot.
    
    Part A: a C loop computes x * x for 1 billion values, first never using the
    result, then summing it and printing the sum (compiled with -O2; needs cc).
    Part B: a program sleeps 2 ms about once a second and does busy work otherwise,
    while a timer-signal sampler records what it is doing at 99 Hz and at 999 Hz.
    Counts the samples that land in the sleep, and the busy work lost to sampling.
    """
    import argparse, json, os, random, signal, subprocess, tempfile, time
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--iter", type=int, default=1_000_000_000, help="Part A: loop iterations.")
    p.add_argument("--seconds", type=int, default=60, help="Part B: profiled seconds per sampling rate.")
    p.add_argument("--sleep-ms", type=float, default=2, help="Part B: length of the once-a-second sleep.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    C = r"""
    #include <stdio.h>
    #include <time.h>
    int main(void){
      struct timespec a,b; clock_gettime(CLOCK_MONOTONIC,&a);
      unsigned long sum = 0;
      for (unsigned long x = 0; x < ITER; x++) {
    #ifdef OBSERVE
        sum += x * x;
        __asm__ volatile("" : "+r"(sum));          /* keeps the compiler from replacing the loop with a formula */
    #else
        unsigned long y = x * x; (void)y;          /* never used */
    #endif
      }
      clock_gettime(CLOCK_MONOTONIC,&b);
      double s = (b.tv_sec-a.tv_sec)+(b.tv_nsec-a.tv_nsec)/1e9;
      fprintf(stderr, "sum=%lu\n", sum);
      printf("%.9f\n", s);
    }
    """
    d = tempfile.mkdtemp(); src = os.path.join(d, "square.c"); open(src, "w").write(C); part_a = []
    for name, flags in (("result never used", []), ("sum printed", ["-DOBSERVE"])):
        exe = os.path.join(d, "observe" if flags else "unused")
        subprocess.run(["cc", "-O2", f"-DITER={args.iter}UL", *flags, src, "-o", exe], check=True)
        sec = float(subprocess.run([exe], capture_output=True, text=True, check=True).stdout)
        part_a.append({"variant": name, "seconds": sec, "ns_per_iter": sec / args.iter * 1e9})
    
    sleeping = False; samples = {"total": 0, "sleep": 0}; hit_events = set(); event = 0
    def sample(signum, frame):
        samples["total"] += 1
        if sleeping: samples["sleep"] += 1; hit_events.add(event)
    
    def profiled(hz):
        global sleeping, event
        samples.update(total=0, sleep=0); hit_events.clear(); event = 0; work = 0
        if hz: signal.signal(signal.SIGALRM, sample); signal.setitimer(signal.ITIMER_REAL, 1 / hz, 1 / hz)
        start = time.perf_counter(); next_sleep = start + 1
        while (now := time.perf_counter()) < start + args.seconds:
            if now >= next_sleep:
                event += 1; sleeping = True; time.sleep(args.sleep_ms / 1000); sleeping = False; next_sleep += random.uniform(0.95, 1.05)   # about once a second: a fixed 1 s would run in lockstep with the timer
            for _ in range(1000): work += 1                                    # busy work
        signal.setitimer(signal.ITIMER_REAL, 0)
        return {"rate": f"{hz} Hz" if hz else "no profiler", "hz": hz, "samples": samples["total"], "in_sleep": samples["sleep"],
                "expected_in_sleep": hz * event * args.sleep_ms / 1000, "sleeps": event, "sleeps_seen": len(hit_events), "work": work}
    
    part_b = [profiled(0), profiled(99), profiled(999)]
    for r in part_b: r["overhead_pct"] = (1 - r["work"] / part_b[0]["work"]) * 100
    
    for r in part_a: print(f"A  {r['variant']:<18} {r['seconds']:.4f} s  ({r['ns_per_iter']:.4f} ns per iteration)")
    for r in part_b[1:]:
        print(f"B  {r['rate']:>6}: {r['samples']} samples, {r['in_sleep']} in the sleep (expected {r['expected_in_sleep']:.1f}); "
              f"{r['sleeps_seen']} of {r['sleeps']} sleeps seen at all; busy work {r['overhead_pct']:.1f}% lower than unprofiled")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"part_a": part_a, "part_b": part_b}}, 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 a-micro-benchmark-that-disappears-and.py
    uv run a-micro-benchmark-that-disappears-and.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--iter1,000,000,000 Part A: loop iterations.
--seconds60 Part B: profiled seconds per sampling rate.
--sleep-ms2 Part B: length of the once-a-second sleep.

Our run

Part A: one billion x*x

Part A: one billion x*x
VariantTime (s)ns per iteration
result never used00
sum printed0.3280.328

Part B: a 2 ms sleep once a second, 60 s

Part B: a 2 ms sleep once a second, 60 s
SamplingSamplesIn the sleepExpectedSleeps seenOverhead
no profiler00000%
99 Hz5.94K2211.722-0.089%
999 Hz59.9K141120600.415%
Reference runApple M2 · 8 cores · 8 GB
Output · uv run a-micro-benchmark-that-disappears-and.py
A  result never used  0.0000 s  (0.0000 ns per iteration)
A  sum printed        0.3282 s  (0.3282 ns per iteration)
B   99 Hz: 5939 samples, 22 in the sleep (expected 11.7); 22 of 59 sleeps seen at all; busy work -0.1% lower than unprofiled
B  999 Hz: 59879 samples, 141 in the sleep (expected 119.9); 60 of 60 sleeps seen at all; busy work 0.4% lower than unprofiled
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

The unused loop took 0 s: the compiler removed it. Summed and printed, it took 0.328 ns per iteration. At 99 Hz, 22 samples landed in the sleep (about 11.7 expected from its share of time), and only 22 of 59 sleeps were seen at all. At 999 Hz, 60 of 60 were seen, and the profiler cost 0.415% of the busy work, too little to measure here.


Read it

The sleep is not invisible at 99 Hz, it is under-sampled: across a minute its total share is estimated about right, while any single occurrence is likely missed. A profile is a statistical estimate of where time goes, and short rare events need either a higher rate or a longer run, never a single snapshot.