Algorithms

Foundations: correctness and asymptotic analysis

12 / 454
Contents

Contents

Algorithms

0 of 454 complete

10 min read

Measuring growth on a real machine

Double the input, watch what the time does, and read the exponent straight off the ratio.

An analysis is a prediction, and a prediction can be checked. Running the program at several sizes and watching how the time responds catches two things nothing else does: an error in the counting, and a constant factor so large that the theoretically better procedure loses on every input you actually have.

The experiment that reveals the most is the simplest one available. Run the same procedure on an input, then on an input twice as large, then twice as large again, and look at the ratio between consecutive times rather than at the times themselves. The first pass is a warm-up whose time is thrown away, because a machine that has been idle takes a moment to come up to speed and would make the smallest size look artificially slow.

Double the input, watch the ratioPython

Change the sizes to 250, 500, 1000, 2000 and run it again. Every number of seconds changes and the ratios stay where they were.

The times depend on the machine, the day and what else is running. The ratios do not: each doubling of the input multiplies the time by roughly 4. That number is the measurement worth writing down, because it names the growth.

Turning a ratio into an exponent

Suppose the running time behaves like T(n)=c nbT(n) = c\,n^b for some constant cc and some power bb. Then doubling the input gives T(2n)=c (2n)b=c 2bnbT(2n) = c\,(2n)^b = c\,2^b n^b, so the ratio is T(2n)/T(n)=2bT(2n)/T(n) = 2^b. The unknown constant cc cancels, which is why the ratio survives a change of machine.

b  =  log⁡2 ⁣(T(2n)T(n))b \;=\; \log_2\!\left(\frac{T(2n)}{T(n)}\right)
The measured order of growth: take the ratio between consecutive doublings and take its logarithm to base 2.

Here is the arithmetic on one measured pair, with every step. Suppose T(1000)=0.0410T(1000) = 0.0410 seconds and T(2000)=0.1652T(2000) = 0.1652 seconds. The ratio is 0.1652/0.0410=4.030.1652 / 0.0410 = 4.03. Now log⁡24.03\log_2 4.03 is the power of 2 that gives 4.03, and since 22=42^2 = 4, that power is a little above 2, in fact 2.01. So b≈2b \approx 2 and the measured growth is quadratic.

Not every growth is a power of nn, and the method still tells you something when it is not. The next snippet takes three sets of measurements already recorded, so the numbers are the same every time you run it, and reports the exponent implied by each doubling.

Three sets of measurements, three verdictsPython

Change one of B's later readings to twice its neighbour and run it again. One noisy reading moves two of the exponents, which is why a single doubling is never enough.

Machine A's ratios are 2, so its exponent is 1 and the growth is linear. Machine C's are 4, so its exponent is 2 and the growth is quadratic. Machine B sits at about 2.18, giving an exponent near 1.13, which is not a whole number and is not meant to be: a cost of nlog⁡2nn\log_2 n has ratio 2(log⁡2n+1)/log⁡2n2(\log_2 n + 1)/\log_2 n, a value that starts above 2 and drifts slowly down towards it as nn grows.

Ratio between consecutive doublingsGrowthBehaviour of the ratio
about 1constant or logarithmicstays at 1; a log adds a little each time
about 2linearsteady at 2
a little above 2, and slowly fallingn log n2.3 at small sizes, 2.1 at large ones
about 4quadraticsteady at 4
about 8cubicsteady at 8
the ratio itself keeps growingexponentialdoubling n squares the time
Reading the doubling ratio. The right-hand column is what the ratio does as the sizes get bigger.

What the model charges nothing for

The RAM model says every memory cell costs the same to reach as every other. That is false on real hardware, and the falsehood does not change the growth. What it changes is the constant, and the constant is what you feel.

The next snippet walks an array of about four million cells and adds up what it finds, twice over. Both walks read four million cells, perform four million additions and finish on the same total. The only difference is the stride, meaning the gap between one position visited and the next: the first walk uses a stride of 1 and steps to the neighbouring cell, the second uses a stride of 4099 and lands a long way off each time.

The remainder operation % n sends a position that has run off the end back round to the start, so both walks stay inside the array and both visit every cell exactly once, in a different order. Predict which one finishes first before you run it.

Same count of operations, different constantPython

Change the second walk(4099) to walk(1) and run it again. Both walks now step through memory the same way, the ratio falls to about 1, and neither version's growth ever changed.

The two walks perform the same number of arithmetic operations and the jumping one takes longer, commonly two or three times longer. The reason is that a machine fetches memory in blocks of neighbouring cells and keeps the blocks it touched recently in a small fast store called a cache. Stepping to the next cell usually finds it there already; jumping 4099 cells usually does not, and the walk waits for main memory.

That gap is invisible to the analysis and decisive in a real program, which is why measurement is a companion to analysis rather than a replacement for it: the analysis tells you which algorithm survives a bigger input, the measurement tells you which implementation to ship today.

A procedure is timed at sizes 1000, 2000, 4000 and 8000, giving 0.010, 0.021, 0.043 and 0.088 seconds. What growth does that suggest, and what would you expect at 64000?

The ratios are 2.1, 2.05 and 2.05, so the implied exponent is about 1.04, which is linear or very close to it, and the small excess is the sort of drift an n log n cost shows. Going from 8000 to 64000 is three more doublings, so the time multiplies by roughly 2 times 2 times 2, which is 8. Expect about 0.7 seconds. If the measurement then comes back at 5 seconds, the model was wrong and the first thing to suspect is that the input no longer fits in cache.

Check yourself

Two implementations of the same task are timed. At n = 1000, A takes 0.5 seconds and B takes 5 seconds. Doubling n multiplies A's time by 4 and B's by 2. Which should you use?

All lessons in Algorithms