The Doubling Experiment
Double n, look at the time: flat, twice as slow, or four times as slow?
- run a doubling experiment on any function;
- read the ratio column and name the growth pattern from it alone;
- draw a labelled plot of time against input size with matplotlib;
- use a log–log plot when the numbers span several orders of magnitude;
- read a growth pattern off the slope of a log–log line, and fit that slope with one line of code;
- recognise the situations where the ratio column quietly misleads you;
- write a short, honest paragraph interpreting your own measurements.
6.1The question that replaces "is it fast?"
"Is my program fast?" is unanswerable — fast compared to what, on whose machine, with how much data? The useful question is:
If I double the input, what happens to the time?
This one question has a small number of possible answers, and each answer identifies a family of algorithms. You do not need mathematics to use it. You need a stopwatch, four input sizes, and a division.
| Time ratio when n doubles | What it means | Typical code shape | Name (week 8) |
|---|---|---|---|
| ≈ 1.0 (unchanged) | Input size is irrelevant | A few lines, no loop over the data | O(1) |
| a fixed tiny addition, not a multiple | Each doubling costs one more step | Halving while loop | O(log n) |
| ≈ 2.0 | Twice the data, twice the work | One loop over the data | O(n) |
| ≈ 2.1 – 2.3 | A loop, plus a little extra per doubling | Sorting | O(n log n) |
| ≈ 4.0 | Twice the data, four times the work | Loop inside a loop | O(n²) |
| ≈ 8.0 | Three nested loops | Triple loop | O(n³) |
Memorise two rows if you memorise nothing else: 2.0 is fine, 4.0 is a warning.
6.2Running the experiment
A reusable harness. Give it a function and a list of sizes, get a table with ratios.
import time
def time_it(func, arg, repeats=5):
best = None
for _ in range(repeats):
start = time.perf_counter()
func(arg)
elapsed = time.perf_counter() - start
if best is None or elapsed < best:
best = elapsed
return best
def doubling(func, start_n=10000, doublings=5):
"""Time func at n, 2n, 4n ... and print the ratio each time."""
sizes, times = [], []
n = start_n
previous = None
print(f"{'n':>9} {'seconds':>12} {'ratio':>7}")
for _ in range(doublings):
t = time_it(func, n)
ratio = t / previous if previous else float('nan')
print(f"{n:>9} {t:>12.5f} {ratio:>7.2f}")
sizes.append(n); times.append(t)
previous = t
n = n * 2
return sizes, timesNow try it on three functions with visibly different shapes:
def constant(n): # ignores n entirely
return n * (n + 1) // 2
def linear(n): # one loop over n
total = 0
for i in range(n):
total += i
return total
def quadratic(n): # loop inside a loop
count = 0
for i in range(n):
for j in range(n):
count += 1
return count
sizes_l, times_l = doubling(linear, start_n=100000)
sizes_q, times_q = doubling(quadratic, start_n=500, doublings=4)You did not need to read the code to tell them apart. The ratio column did it: 2.0 versus 4.0. This is the core practical skill of the course, and you now have it.
Notice that the quadratic experiment starts at n = 500, not 100 000 — a quadratic function at 100 000 would run for hours. Choosing a starting size that finishes but is still big enough to beat the noise is part of the craft. Aim for a first run of roughly 0.01–0.1 seconds.
How do you pick that starting size without guessing? Run the function once at a small
n, look at the time, and scale up. If your shape is linear and n = 1 000
took 0.00001 s, then 0.05 s needs about 5 000 times more work, so start near
n = 5 000 000. If the shape is quadratic, remember that the time grows with the
square: a 100× longer run only needs a 10× bigger n. A quick pilot run saves you
from either a wall of noise or a coffee break.
6.3Drawing the picture
A table convinces you; a plot convinces everyone else. Matplotlib is already installed in Colab.
import matplotlib.pyplot as plt
plt.figure(figsize=(6, 4))
plt.plot(sizes_l, times_l, marker="o", label="one loop")
plt.plot(sizes_q, times_q, marker="s", label="loop inside a loop")
plt.xlabel("input size n")
plt.ylabel("time (seconds)")
plt.title("Running time vs input size")
plt.legend()
plt.grid(alpha=0.3)
plt.show()Every plot you hand in must have: axis labels with units, a title, and a legend if there is more than one line. A plot without labels is not evidence, it is decoration.
When the numbers are wildly different
If one curve is a million times taller than another, the small one flattens into the axis. Switch both axes to logarithmic scale and each straight line's slope tells you the growth: slope 1 is linear, slope 2 is quadratic, flat is constant.
plt.xscale("log")
plt.yscale("log")6.4Reading — and measuring — the slope
Why does a log–log plot turn every growth pattern into a straight line? A little algebra makes it click. Almost every function here has the shape "some constant times n to a power":
T = c · nk
Take the logarithm of both sides and the multiplication becomes addition, the power becomes a multiplier:
log T = log c + k · log n
That is the equation of a straight line — log n along the bottom,
log T up the side, a slope of exactly k, and the constant
c only shifting the line up or down without tilting it. So on log–log axes
the power becomes the slope, and the machine-dependent constant becomes
an offset you can ignore. Constant → slope 0 (flat). Linear → slope 1. Quadratic →
slope 2. Cubic → slope 3.
You do not have to eyeball the slope. numpy.polyfit fits the best straight
line through your logged points and hands you the slope as a number:
import numpy as np
def loglog_slope(sizes, times):
"""Best-fit slope through the points on log-log axes."""
slope, intercept = np.polyfit(np.log(sizes), np.log(times), 1)
return slope
print(f"linear slope = {loglog_slope(sizes_l, times_l):.2f}")
print(f"quadratic slope = {loglog_slope(sizes_q, times_q):.2f}")This is the ratio column's more grown-up cousin. The ratio reads growth from two points at a time; the fitted slope uses all your points at once, so a single noisy measurement cannot swing the verdict. When a ratio column is bouncing around, fit the slope and see the pattern the individual ratios were hiding.
A slope of k means doubling n multiplies the time by
2k. Slope 1 → ratio 2. Slope 2 → ratio 4. Slope 3 → ratio 8.
Slope 0 → ratio 1. They are two readings of one truth: the ratio is local and quick,
the slope is global and steady.
6.5When the ratio column misleads you
The ratio is a wonderful tool, but a tool you trust blindly will eventually cut you. Three situations make honest ratios lie about the pattern.
1. The starting size is too small
Below the noise floor the ratio is meaningless — it wanders 0.6, 1.9, 0.4 with no shape at all, because you are timing interruptions, not your algorithm. This is the number one cause of a "weird" ratio column, and the fix is always the same: grow the starting size until the first run clears roughly 0.01–0.1 seconds, then read again.
2. n log n looks like a drifting 2
The most subtle case. A sorting run does not give a clean 2.0 or 4.0 — it gives a ratio just above 2 that slowly drifts downward towards 2 as n grows:
| n | ratio for n·log₂n | reading |
|---|---|---|
| 1 000 → 2 000 | 2.20 | above 2, so worse than linear |
| 2 000 → 4 000 | 2.18 | … |
| 4 000 → 8 000 | 2.17 | …creeping down… |
| 1 000 000 → 2 000 000 | 2.10 | …towards 2, but never reaching it |
The reason: doubling n doubles the loop (that is the 2), and the log n
factor adds only a sliver more each time — a sliver that shrinks as n grows, because the
logarithm grows ever more slowly. Read too high a starting size and you might call it
linear; read too low and you might call it quadratic. The honest verdict is "just above
2 and falling slowly", the fingerprint of n log n — confirm it by fitting a slope
(§6.4), which lands near 1.05–1.1.
3. Memory effects can inflate a linear ratio
A perfectly linear loop can show ratios above 2 once the data grows too big to sit in the CPU's fast cache. Suddenly each item costs a slow trip to main memory, and the time per item rises just as n rises. The algorithm did not change class; the hardware changed gear. If a clean O(n) function starts posting 2.3, 2.6 at very large sizes, suspect the cache before you rewrite the algorithm.
When a ratio surprises you, do not immediately believe it. Ask the three questions in order: is the run long enough, could this be n log n drifting, and has the data outgrown the cache? Then fit a slope across all your points as a tie-breaker.
6.6Interpreting like a scientist
Every benchmark in this course ends with a short paragraph. Use this skeleton until it becomes automatic:
- What I measured, and at which sizes.
- What the ratio column did (the numbers, not an adjective).
- Which growth pattern that matches, and why the code has that shape.
- One thing that could have distorted the measurement.
- What I would predict for an input ten times larger — a specific number.
Worked example, for the quadratic run above:
"I timed the nested-loop counter at n = 500, 1 000, 2 000 and 4 000, five repeats each, taking the fastest. The time ratios were 3.99, 4.02 and 3.99, and a log–log fit gave a slope of 2.00. Four times the work for twice the data matches a loop inside a loop, which is exactly what the code does: the inner loop runs n times for each of the n outer passes. The 4 000 run took nearly a second, so background load on the shared Colab machine could have inflated it slightly. Extrapolating, n = 40 000 would be 100× the 4 000 time — about 80 seconds — which I would not want to run in a live report."
6.7Try it yourself
Someone hands you these timings. Name each pattern without seeing the code.
| n | A (s) | B (s) | C (s) | D (s) |
|---|---|---|---|---|
| 1 000 | 0.0021 | 0.0000004 | 0.0100 | 0.0000060 |
| 2 000 | 0.0042 | 0.0000004 | 0.0401 | 0.0000065 |
| 4 000 | 0.0084 | 0.0000004 | 0.1602 | 0.0000070 |
| 8 000 | 0.0167 | 0.0000004 | 0.6410 | 0.0000075 |
Answers
A doubles each time — one loop over the data (linear). B never changes — constant. C quadruples — nested loops (quadratic). D grows by a small fixed amount each doubling, not a multiple — that is the halving pattern, logarithmic.
Run doubling() on: (a) summing a list, (b) the worst-case
contains search from week 5, (c) the duplicate-finder from week 4, and
(d) sorted(data) on a shuffled list. Record all four ratio columns.
What to expect
(a) and (b) ≈ 2.0; (c) ≈ 4.0; (d) ≈ 2.1–2.2. That last one is the fingerprint of n log n — clearly worse than a plain loop, dramatically better than nested loops. Week 13 explains where it comes from.
Plot (b) and (c) from Task 2 on the same axes with labels, title and legend. Then make a second version with log–log axes. Which version communicates better, and to whom?
Hint
The linear-axis plot shows the practical horror (one curve shoots off the top). The log–log plot shows the structure (two straight lines with different slopes). Reports for decision-makers usually want the first; technical appendices want the second.
Write the loglog_slope helper from §6.4 and run it on all four studies
from Task 2. Line the fitted slopes up against the last ratio in each column. Do slope
and ratio agree (slope 1 ↔ ratio 2, slope 2 ↔ ratio 4)? Where they disagree, which do
you trust more, and why?
Expected
(a) and (b) fit near 1.0, (c) near 2.0, (d) near 1.05–1.1. The slope is steadier because it uses every point at once; a lone noisy ratio cannot tilt it. When a single ratio looks odd but the slope is clean, trust the slope.
Run doubling(sorted_wrapper, start_n=10000, doublings=7) where
sorted_wrapper(n) shuffles a list of n items and sorts it. Read the ratio
column top to bottom. Does it settle on a single number, or drift? Explain what you see
using §6.5, and give the fitted slope.
Expected
The ratio hovers a little above 2 and drifts slowly downward as n grows — never landing on a clean value — because the log factor adds an ever-smaller sliver each doubling. The fitted slope comes out near 1.05–1.1, which is the honest label: n log n, distinctly worse than linear but far better than quadratic.
Run your linear study twice: once with start_n=10 and once with
start_n=1000000, four doublings each. Compare the two ratio columns. Which
one gives clean 2.0s, and what does the other one teach you about trusting a ratio?
Answer
The tiny start gives a jumpy, meaningless column (0.7, 1.8, 1.1…) because every run is buried in noise; the large start gives steady 2.0s. Same algorithm, same code — only the size changed. A ratio is only worth reading once the run is well clear of the noise floor (§6.5, case 1).
6.8Self-check
Doubling n makes the time go from 0.10 s to 0.41 s. The most likely shape is:
Times at n = 1 000 / 2 000 / 4 000 are 0.0000060 / 0.0000065 / 0.0000070 s. This is:
Your first timing point is 0.00003 s and the ratios are 0.7, 1.9, 0.4. The best next move is:
On a log–log plot your measurements form a straight line of slope 2. The algorithm is:
A sorting benchmark posts ratios 2.20, 2.18, 2.16, 2.15 as n grows. The best reading is:
6.9Homework
- Create
AA_Week06.ipynbwith thetime_itanddoublingharness, plus theloglog_slopehelper. - Complete Task 2 (four studies), Task 3 (two figures) and Task 4 (fitted slopes for all four).
- Complete Task 5 (watch n log n drift) and Task 6 (the misleading start size), and write one sentence on what each taught you about reading a ratio.
- For each of the four studies, write the five-sentence interpretation from §6.6, quoting both the last ratio and the fitted slope.
- Add one prediction and test it: pick any study, predict the time at the next doubling before running it, then run it and report the error as a percentage.
6.10Words from this week
| Term | Meaning in plain words |
|---|---|
| doubling experiment | Measuring at n, 2n, 4n… and watching the ratio of times. |
| ratio column | Each time divided by the previous one — the machine-independent part of the result. |
| log–log plot | Both axes logarithmic; growth patterns become straight lines with distinct slopes. |
| slope (log–log) | The power in T = c·nᵏ; slope 1 is linear, 2 is quadratic, 0 is constant. |
| line fitting | Using every point at once (e.g. numpy.polyfit) to read a steady slope. |
| cache effect | Data outgrowing fast memory, which can push a linear ratio above 2. |
| extrapolation | Using a measured pattern to predict a size you have not run. |
| harness | Reusable code that runs and times an experiment for you. |
The ratio column revealed the shape, but the seconds drift with your laptop. Week 7 switches to counting steps — a measure that does not change when the hardware does.