ЁЯПл The SchoolтА║ЁЯПОя╕П PerformanceтА║ЁЯПБ рдзрдбрд╛ 02 тАФ рдкреНрд░рд╛рдорд╛рдгрд┐рдХ benchmarking: warm-up рдлреЗрд▒реНрдпрд╛ рдЖрдгрд┐ рдЕрдиреЗрдХ heats
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯПБ рдзрдбрд╛ 02 тАФ рдкреНрд░рд╛рдорд╛рдгрд┐рдХ benchmarking: warm-up рдлреЗрд▒реНрдпрд╛ рдЖрдгрд┐ рдЕрдиреЗрдХ heats

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 02 ┬╖ рдорд╛рдЧреЗ: lesson-01-latency-percentiles ┬╖ рдкреБрдвреЗ: lesson-03-profiling


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбрд╛ 01, рдЕрдзрд┐рдХ рдПрдХрд╛ program рдЪреНрдпрд╛ рджреЛрди versions рдЪреА рдкреНрд░рд╛рдорд╛рдгрд┐рдХ рддреБрд▓рдирд╛. рдПрдХ run рдЦреЛрдЯреЗ рдмреЛрд▓рддреЛ: рдкрд╣рд┐рд▓реЗ runs рдердВрдб рдЕрд╕рддрд╛рдд, machine рдордзреНрдпреЗ noise рдЕрд╕рддреЛ, рдЖрдгрд┐ рдЫреЛрдЯреЗ рдмрджрд▓ рддреНрдпрд╛ noise рдордзреНрдпреЗ рд▓рдкрддрд╛рдд. рдпрд╛рд╡рд░ рдЙрдкрд╛рдп: warm up рдХрд░рд╛, рдкреБрдиреНрд╣рд╛ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рд╛, median рдЖрдгрд┐ рдкрд╕рд╛рд░рд╛ (spread) рд╕рд╛рдВрдЧрд╛, рдЖрдгрд┐ рдлрд╛рдпрджрд╛ рддреЗрд╡реНрд╣рд╛рдЪ рдЬрд╛рд╣реАрд░ рдХрд░рд╛ рдЬреЗрд╡реНрд╣рд╛ рддреЛ noise рдкреЗрдХреНрд╖рд╛ рдореЛрдард╛ рдЕрд╕реЗрд▓. perf/demo.py рдордзрд▓реЗ bench() рдЖрдгрд┐ perf/sim.py рдордзрд▓реЗ simulated_runs, spread рдЖрдгрд┐ verdict.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдирд╡реАрди рдзрд╛рд╡рдгреНрдпрд╛рдЪреНрдпрд╛ рдмреБрдЯрд╛рдВрдореБрд│реЗ рдРрд╢реНрд╡рд░реНрдпрд╛ рдЬрд▓рдж рдзрд╛рд╡рддреЗ рдХрд╛, рд╣реЗ рджреАрдкрд┐рдХрд╛рд▓рд╛ рдЬрд╛рдгреВрди рдШреНрдпрд╛рдпрдЪреЗ рдЖрд╣реЗ. ЁЯСЯ

рдХрддрд░рд┐рдирд╛ рдЬреБрдиреНрдпрд╛ рдмреБрдЯрд╛рдВрдд рдРрд╢реНрд╡рд░реНрдпрд╛рдЪреА рд╡реЗрд│ рдПрдХрджрд╛рдЪ рдШреЗрддреЗ. рднрд▓реНрдпрд╛ рд╕рдХрд╛рд│реА рдРрд╢реНрд╡рд░реНрдпрд╛ рдердВрдб рдЖрдгрд┐ рдЖрдЦрдбрд▓реЗрд▓реА рдЕрд╕рддреЗ: 42 seconds. рдирдВрддрд░, рдирд╡реАрди рдмреБрдЯрд╛рдВрдд: 12 seconds. "рдирд╡реАрди рдмреВрдЯ 3 рдкрдЯ рдЬрд▓рдж рдЖрд╣реЗрдд!" рдирд╛рд╣реА тАФ рдРрд╢реНрд╡рд░реНрдпрд╛ рдлрдХреНрдд warm up рд╣реЛрдд рд╣реЛрддреА.

рдореНрд╣рдгреВрди рддреНрдпрд╛ рд╣реЗ рдиреАрдЯ рдХрд░рддрд╛рдд:

рдордЧ рддреНрдпрд╛ рддреБрд▓рдирд╛ рдХрд░рддрд╛рдд. рдЬреБрдиреНрдпрд╛ рдЖрдгрд┐ рдирд╡реНрдпрд╛ рд╡реЗрд│рд╛ рдЦреВрдк overlap рд╣реЛрдд рдЕрд╕рддреАрд▓, рддрд░ рдмреБрдЯрд╛рдВрдиреА рдХреЛрдгрддрд╛рд╣реА рд╕реНрдкрд╖реНрдЯ рдлрд░рдХ рдХреЗрд▓рд╛ рдирд╛рд╣реА. рдирд╡реНрдпрд╛ рд╡реЗрд│рд╛ рд╕реНрдкрд╖реНрдЯрдкрдгреЗ рдХрдореА рдЕрд╕рддреАрд▓ рддрд░рдЪ рддреЛ рдЦрд░рд╛ рдлрд╛рдпрджрд╛ рдЖрд╣реЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    cold["ЁЯе╢ 3 cold runs<br/>42.1 ┬╖ 31.7 ┬╖ 21.6 ms"] -->|"drop them"| warm["ЁЯФБ 17 warm runs"]
    warm --> med["median 12.4 ms<br/>middle half 11.7тАУ12.7"]
    b1["B1 tweak: middle half 11.3тАУ12.3"] --> cmp{"do the middle<br/>halves overlap?"}
    b2["B2 fix: middle half 7.8тАУ8.4"] --> cmp
    med --> cmp
    cmp -->|"B1 overlaps"| no["no clear difference"]
    cmp -->|"B2 is clear"| yes["B is faster (1.53x)"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l02

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдкреБрдврдЪрд╛ рдкреНрд░рддреНрдпреЗрдХ рдзрдбрд╛ "рдЬрд▓рдж рдХрд░рдгреНрдпрд╛рд╕рд╛рдареА" code рдмрджрд▓рддреЛ. рдкреНрд░рд╛рдорд╛рдгрд┐рдХ рдореЛрдЬрдорд╛рдкрд╛рд╢рд┐рд╡рд╛рдп рддреБрдореНрд╣реА рдХрд╛рд╣реАрдЪ рди рдХрд░рдгрд╛рд░реЗ рдмрджрд▓ рдареЗрд╡рд╛рд▓, рдорджрдд рдХрд░рдгрд╛рд░реЗ рдмрджрд▓ рдлреЗрдХреВрди рджреНрдпрд╛рд▓, рдЖрдгрд┐ noise рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдЖрдХрдбреНрдпрд╛рдВрд╡рд░ рд╡рд╛рдж рдШрд╛рд▓рд╛рд▓. рдЬреЛ benchmark 3% рдмрджрд▓ рдкрд╛рд╣реВ рд╢рдХрдд рдирд╛рд╣реА, рддреЛ рддрд╕рд╛ рджрд╛рд╡рд╛ рдХрд░рдгреНрдпрд╛рд╕рд╛рдареА рд╡рд╛рдкрд░реВ рдирдпреЗ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

perf/sim.py рдордзрд▓реЗ simulated_runs(base_ms, n, seed) рд╣реЗ рдПрдХ simulated рд╕реНрдЯреЙрдкрд╡реЙрдЪ рдЖрд╣реЗ: рдкреНрд░рддреНрдпреЗрдХ run рд▓рд╛ base_ms ┬▒ 8% (seeded) рд▓рд╛рдЧрддреЛ, рдкрд╣рд┐рд▓реЗ 3 runs warm-up рдЦрд░реНрдЪ рдЬреЛрдбрддрд╛рдд (30, 20, 10 ms), рдЖрдгрд┐ 5% runs рдПрдХ spike рдЬреЛрдбрддрд╛рдд. spread(xs) min, p25, median, p75 рдЖрдгрд┐ max рдкрд░рдд рджреЗрддреЗ. verdict(a, b) рд╡рд░рдЪрд╛ рдЕрдВрджрд╛рдЬреА рдирд┐рдпрдо рд▓рд╛рдЧреВ рдХрд░рддреЗ. perf/demo.py рдордзрд▓реЗ bench() version A (12 ms), рдЫреЛрдЯрд╛ tweak B1 (11.7 ms) рдЖрдгрд┐ рдЦрд░рд╛ fix B2 (8 ms) рдпрд╛рдВрдЪреА рд╡реЗрд│ рдШреЗрддреЗ. рдХрд░реВрди рдкрд╛рд╣рд╛ рдЪреА рд╢реЗрд╡рдЯрдЪреА рдУрд│ Python рдЪреЗ рдЦрд░реЗ timeit рд╡рд╛рдкрд░рддреЗ тАФ рдЦрд░реЗ рдШрдбреНрдпрд╛рд│.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 perf/demo.py bench
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); from sim import simulated_runs, spread, verdict
a = simulated_runs(12.0)[3:]
for base in (12.0, 11.5, 11.0, 10.5, 10.0, 9.0):
    b = simulated_runs(base, seed=6)[3:]
    print(f"B at {base:>4} ms тЖТ median {spread(b)['median']:>4} ┬╖ {verdict(a, b)}")
for n in (3, 5, 10, 20):
    print(f"{n:>2} runs, warm-up kept тЖТ median {spread(simulated_runs(12.0, n=n))['median']:>4} ms")
EOF
python3 -m timeit -n 1000 -r 5 "sorted(range(1000, 0, -1))"

рд╢реЗрд╡рдЯрдЪрд╛ command рддреБрдордЪреНрдпрд╛ machine рд╡рд░ рдЦрд▒реНрдпрд╛ sort рдЪреА рд╡реЗрд│ рдШреЗрддреЛ тАФ рдХрд╛рд╣реАрд╕реЗ рдЕрд╕реЗ: 1000 loops, best of 5: 5.62 usec per loop. рддреБрдордЪреЗ рдЖрдХрдбреЗ рд╡реЗрдЧрд│реЗ рдЕрд╕рддреАрд▓, рдкреНрд░рддреНрдпреЗрдХ run рд▓рд╛ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ machine рд╡рд░; рд╣рд╛рдЪ рддрд░ рдпрд╛ рдзрдбреНрдпрд╛рдЪрд╛ рдореБрджреНрджрд╛ рдЖрд╣реЗ.

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

bench рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА Katrina times version A 20 times (simulated stopwatch): first three runs [42.1, 31.7, 21.6] ms тАФ cold, warming up
   one run says 42.1 ms ┬╖ the mean of all 20 says 15.5 ms ┬╖ after 3 warm-up runs, the median of 17 says 12.4 ms
   spread of the 17: min 11.2 ┬╖ p25 11.7 ┬╖ median 12.4 ┬╖ p75 12.7 ┬╖ max 19.9 (one spike: another program woke up)
тФАтФА A vs B1 (a small tweak): medians 12.4 vs 12.0 ms ┬╖ middle halves 11.7тАУ12.7 vs 11.3тАУ12.3 тЖТ no clear difference: the change is smaller than the noise
тФАтФА A vs B2 (a real fix): medians 12.4 vs 8.1 ms ┬╖ middle halves 11.7тАУ12.7 vs 7.8тАУ8.4 тЖТ B is faster (1.53x)

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

B at 12.0 ms тЖТ median 12.3 ┬╖ no clear difference: the change is smaller than the noise
B at 11.5 ms тЖТ median 11.8 ┬╖ no clear difference: the change is smaller than the noise
B at 11.0 ms тЖТ median 11.3 ┬╖ B is faster (1.10x)
B at 10.5 ms тЖТ median 10.8 ┬╖ B is faster (1.15x)
B at 10.0 ms тЖТ median 10.2 ┬╖ B is faster (1.22x)
B at  9.0 ms тЖТ median  9.2 ┬╖ B is faster (1.35x)
 3 runs, warm-up kept тЖТ median 31.7 ms
 5 runs, warm-up kept тЖТ median 21.6 ms
10 runs, warm-up kept тЖТ median 12.9 ms
20 runs, warm-up kept тЖТ median 12.6 ms

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдПрдХрд╛ рдердВрдб run рдиреЗ 42.1 ms рд╕рд╛рдВрдЧрд┐рддрд▓реЗ; рдкреНрд░рд╛рдорд╛рдгрд┐рдХ рдЙрддреНрддрд░ 12.4 ms рдЖрд╣реЗ тАФ 3 рдкрдЯреАрдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдХрдореА. рд╕рдЧрд│реНрдпрд╛ 20 рдЪрд╛ mean (15.5) warm-up рдЖрдгрд┐ spike рдореБрд│реЗ рдЕрдЬреВрдирд╣реА рд╡рд░ рдУрдврд▓рд╛ рдЧреЗрд▓рд╛ рд╣реЛрддрд╛. рдЗрддрдХреНрдпрд╛ noise рдордзреНрдпреЗ, рд╕реБрдорд╛рд░реЗ 4% рдЪрд╛ рдмрджрд▓ (12.0 тЖТ 11.5) рджрд┐рд╕рддрдЪ рдирд╛рд╣реА; рд╕реБрдорд╛рд░реЗ 8% (тЖТ 11.0) рд╣рд╛ рдпрд╛рдкреИрдХреА рддреЛ рдкрд╛рд╣реВ рд╢рдХрдгрд╛рд░рд╛ рд╕рд░реНрд╡рд╛рдд рд▓рд╣рд╛рди рдЯрдкреНрдкрд╛ рдЖрд╣реЗ. рдЖрдгрд┐ рдлрдХреНрдд 3 рдХрд┐рдВрд╡рд╛ 5 runs рдЖрдгрд┐ warm-up рди рдХрд╛рдврддрд╛, median рд╕реНрд╡рддрдГрдЪ рдЪреБрдХреАрдЪрд╛ рдпреЗрддреЛ. рдЬрд╛рд╕реНрдд runs рдЖрдгрд┐ warm-up benchmark рдЕрдзрд┐рдХ рдзрд╛рд░рджрд╛рд░ рдХрд░рддрд╛рдд.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ machine рд╡рд░ тАФ pyperf рдкреНрд░рддреНрдпреЗрдХ benchmark рдЕрдиреЗрдХ рдирд╡реНрдпрд╛ processes рдордзреНрдпреЗ рдЪрд╛рд▓рд╡рддреЛ, loop count calibrate рдХрд░рддреЛ, warm-ups рдХрд╛рдвреВрди рдЯрд╛рдХрддреЛ рдЖрдгрд┐ mean ┬▒ standard deviation рд╕рд╛рдВрдЧрддреЛ:

python3 -m pip install pyperf
python3 -m pyperf timeit -o old.json -s "data = list(range(1000, 0, -1))" "sorted(data)"
# change the code, then:
python3 -m pyperf timeit -o new.json -s "data = list(range(1000, 0, -1))" "sorted(data)"
python3 -m pyperf compare_to old.json new.json     # says if the difference is significant
python3 -m pyperf system tune                       # (Linux, root) quieter CPU settings for benchmarking

hyperfine рд╕рдВрдкреВрд░реНрдг commands рд╕рд╛рдареА рд╣реЗрдЪ рдХрд░рддреЛ, warm-up runs рд╕рд╣:

hyperfine --warmup 3 --runs 20 'python3 old_report.py' 'python3 new_report.py'

рддреБрдордЪреНрдпрд╛ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ code рдордзреНрдпреЗ, monotonic рдШрдбреНрдпрд╛рд│ рд╡рд╛рдкрд░рд╛:

import time
t0 = time.perf_counter()
build_results()
print(f"{(time.perf_counter() - t0) * 1000:.1f} ms")

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: benchmark results рддреЗ рдмрдирд╡рдгрд╛рд▒реНрдпрд╛ commit рд╕реЛрдмрдд рдареЗрд╡рд╛, рддреЗ рддреНрдпрд╛рдЪ рд╢рд╛рдВрдд machine рд╡рд░ рдЪрд╛рд▓рд╡рд╛, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдЖрдХрдбреНрдпрд╛рд╢реЗрдЬрд╛рд░реА рдкрд╕рд╛рд░рд╛ рд▓рд┐рд╣рд╛. рдкрд╕рд╛рд▒реНрдпрд╛рдкреЗрдХреНрд╖рд╛ рд▓рд╣рд╛рди speed-up рд╣рд╛ result рдирд╛рд╣реА.

тПня╕П рдкреБрдвреЗ

рдЖрддрд╛ рддреБрдореНрд╣реА рдПрдЦрд╛рджрд╛ program рдореЛрдЬреВ рд╢рдХрддрд╛. рдкрдг рддреНрдпрд╛рдЪрд╛ рдХреЛрдгрддрд╛ рднрд╛рдЧ рд╣рд│реВ рдЖрд╣реЗ? рдкреБрдвреЗ: рдкреНрд░рд╢рд┐рдХреНрд╖рд┐рдХреЗрдЪрд╛ clipboard тАФ profiling.

git checkout lesson-03-profiling

ЁЯПБ Lesson 02 тАФ Benchmarking honestly: warm-up laps and many heats

ЁЯУН You are here: Lesson 02 of 12 ┬╖ Previous: lesson-01-latency-percentiles ┬╖ Next: lesson-03-profiling


ЁЯУж What's in this branch

Lesson 01, plus comparing two versions of a program honestly. One run lies: the first runs are cold, the machine is noisy, and small changes hide inside that noise. The cure: warm up, repeat, report the median and the spread, and only call a win when it is bigger than the noise. bench() in perf/demo.py and simulated_runs, spread and verdict in perf/sim.py.

ЁЯзТ Explain like I'm 5

Dipika wants to know if new running shoes make Aishwarya faster. ЁЯСЯ

Katrina times Aishwarya once in the old shoes. It is early morning, Aishwarya is cold and stiff: 42 seconds. Later, in the new shoes: 12 seconds. "The new shoes are 3 times faster!" No тАФ Aishwarya was just warming up.

So they do it properly:

Then they compare. If the old and new times overlap a lot, the shoes made no clear difference. Only if the new times are clearly lower is it a real win.

ЁЯЧ║я╕П Diagram

flowchart LR
    cold["ЁЯе╢ 3 cold runs<br/>42.1 ┬╖ 31.7 ┬╖ 21.6 ms"] -->|"drop them"| warm["ЁЯФБ 17 warm runs"]
    warm --> med["median 12.4 ms<br/>middle half 11.7тАУ12.7"]
    b1["B1 tweak: middle half 11.3тАУ12.3"] --> cmp{"do the middle<br/>halves overlap?"}
    b2["B2 fix: middle half 7.8тАУ8.4"] --> cmp
    med --> cmp
    cmp -->|"B1 overlaps"| no["no clear difference"]
    cmp -->|"B2 is clear"| yes["B is faster (1.53x)"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l02

тЭУ What

ЁЯдФ Why

Because every later lesson changes code "to make it faster". Without honest measuring you will keep changes that do nothing, throw away changes that help, and argue about numbers that were noise. A benchmark that cannot see a 3% change should not be used to claim one.

ЁЯФз How (in this repo)

simulated_runs(base_ms, n, seed) in perf/sim.py is a simulated stopwatch: each run costs base_ms ┬▒ 8% (seeded), the first 3 runs add a warm-up cost (30, 20, 10 ms), and 5% of runs add a spike. spread(xs) returns min, p25, median, p75 and max. verdict(a, b) applies the rule of thumb above. bench() in perf/demo.py times version A (12 ms), a small tweak B1 (11.7 ms) and a real fix B2 (8 ms). The last line of Try it uses Python's real timeit тАФ a real clock.

ЁЯзк Try it

python3 perf/demo.py bench
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); from sim import simulated_runs, spread, verdict
a = simulated_runs(12.0)[3:]
for base in (12.0, 11.5, 11.0, 10.5, 10.0, 9.0):
    b = simulated_runs(base, seed=6)[3:]
    print(f"B at {base:>4} ms тЖТ median {spread(b)['median']:>4} ┬╖ {verdict(a, b)}")
for n in (3, 5, 10, 20):
    print(f"{n:>2} runs, warm-up kept тЖТ median {spread(simulated_runs(12.0, n=n))['median']:>4} ms")
EOF
python3 -m timeit -n 1000 -r 5 "sorted(range(1000, 0, -1))"

The last command times a real sort on your machine тАФ something like 1000 loops, best of 5: 5.62 usec per loop. Your numbers will differ, run to run and machine to machine; that is the point of this lesson.

тЬЕ Verify тАФ what you should see

bench prints:

тФАтФА Katrina times version A 20 times (simulated stopwatch): first three runs [42.1, 31.7, 21.6] ms тАФ cold, warming up
   one run says 42.1 ms ┬╖ the mean of all 20 says 15.5 ms ┬╖ after 3 warm-up runs, the median of 17 says 12.4 ms
   spread of the 17: min 11.2 ┬╖ p25 11.7 ┬╖ median 12.4 ┬╖ p75 12.7 ┬╖ max 19.9 (one spike: another program woke up)
тФАтФА A vs B1 (a small tweak): medians 12.4 vs 12.0 ms ┬╖ middle halves 11.7тАУ12.7 vs 11.3тАУ12.3 тЖТ no clear difference: the change is smaller than the noise
тФАтФА A vs B2 (a real fix): medians 12.4 vs 8.1 ms ┬╖ middle halves 11.7тАУ12.7 vs 7.8тАУ8.4 тЖТ B is faster (1.53x)

Your snippet prints:

B at 12.0 ms тЖТ median 12.3 ┬╖ no clear difference: the change is smaller than the noise
B at 11.5 ms тЖТ median 11.8 ┬╖ no clear difference: the change is smaller than the noise
B at 11.0 ms тЖТ median 11.3 ┬╖ B is faster (1.10x)
B at 10.5 ms тЖТ median 10.8 ┬╖ B is faster (1.15x)
B at 10.0 ms тЖТ median 10.2 ┬╖ B is faster (1.22x)
B at  9.0 ms тЖТ median  9.2 ┬╖ B is faster (1.35x)
 3 runs, warm-up kept тЖТ median 31.7 ms
 5 runs, warm-up kept тЖТ median 21.6 ms
10 runs, warm-up kept тЖТ median 12.9 ms
20 runs, warm-up kept тЖТ median 12.6 ms

ЁЯПБ What you just proved

One cold run said 42.1 ms; the honest answer is 12.4 ms тАФ more than 3 times less. The mean of all 20 (15.5) was still pulled up by the warm-up and the spike. With this much noise, a change of about 4% (12.0 тЖТ 11.5) is invisible; about 8% (тЖТ 11.0) is the smallest of these steps it can see. And with only 3 or 5 runs and no warm-up, the median itself is wrong. More runs and a warm-up make the benchmark sharper.

тЪая╕П Common mistakes

ЁЯПн In production

On a real machine тАФ pyperf runs each benchmark in several fresh processes, calibrates the loop count, drops warm-ups and reports mean ┬▒ standard deviation:

python3 -m pip install pyperf
python3 -m pyperf timeit -o old.json -s "data = list(range(1000, 0, -1))" "sorted(data)"
# change the code, then:
python3 -m pyperf timeit -o new.json -s "data = list(range(1000, 0, -1))" "sorted(data)"
python3 -m pyperf compare_to old.json new.json     # says if the difference is significant
python3 -m pyperf system tune                       # (Linux, root) quieter CPU settings for benchmarking

hyperfine does the same for whole commands, with warm-up runs:

hyperfine --warmup 3 --runs 20 'python3 old_report.py' 'python3 new_report.py'

In your own code, use a monotonic clock:

import time
t0 = time.perf_counter()
build_results()
print(f"{(time.perf_counter() - t0) * 1000:.1f} ms")

ЁЯПн Why this matters in production: keep benchmark results with the commit that produced them, run them on the same quiet machine, and write the spread next to every number. A speed-up smaller than the spread is not a result.

тПня╕П Next

You can now measure a program. But which part of it is slow? Next: the coach's clipboard тАФ profiling.

git checkout lesson-03-profiling
тЖР Previouslatency percentilesNext тЖТprofiling

This page is the lesson's README from the lesson-02-benchmarking branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.