Counting

Counting the coin flips

A skip list spends 2.03 random bits per key and a treap spends exactly 32. Reservoir sampling spends 1,356,399 bits on a stream of 65,536 and a better version spends 9,380. None of those numbers appears in any complexity class any of these structures is described by, and none of the site's other three counters can see them.

This site counts three things. Comparisons, because that is what a textbook analysis counts and it is exact. Cache misses, because comparisons are not time and locality decides the constant. Slots, because an in-place sort and an out-of-place one with identical comparison counts are different algorithms and nothing else can say so.

Each of those counters was added because a claim could not otherwise be checked. This essay adds a fourth for the same reason: a randomised algorithm consumes randomness, the amount differs by orders of magnitude between algorithms that produce identical answers, and none of the other three counters can see any of it.

Random bits consumed against n, k = 16Coin flips charged at the point they are spent, on logarithmic axes, with the class each consumer declares fitted by the same ratio test the comparison counts get. Rejections are charged: a draw below a range that is not a power of two costs whatever the rejection loop actually spent, not the ⌈log₂ k⌉ it would cost if the arithmetic were free. At n = 16,384 the consumers drawn here span 32,995 to 524,288 bits.10³10⁴10³10⁴10⁵nrandom bitsskip list, one build — ntreap, one build — nreservoir, Algorithm R — n log nn from 256 to 16,384bits charged including rejections
Fig. 1 Random bits charged at the point they are spent, on the same logarithmic frame the comparison counts use, and fitted by the same ratio test. A skip list spends two bits per key; a treap spends thirty-two, because it draws a whole priority where a skip list draws a level; Algorithm R for reservoir sampling spends Θ(n log n) bits for a sample whose size never changes. All three are correct algorithms and none of these differences changes any answer.

The instrument

Coins is to randomness what Counted is to comparisons. Nothing in the randomness library can flip a coin without the flip being charged, which turns “how much randomness does this need” from an estimate into an integer.

Three decisions in the cost model are worth stating, because a cost model is a choice and every figure that reports these numbers prints which one was used.

The unit is a bit, not a call. An algorithm that draws one 32-bit uniform and one that draws thirty-two single bits have made a different number of calls and consumed the same randomness. Counting calls would make Algorithm L look thirty-two times better than it is, because its draws are wide. Both counts are kept — draws and bits — and the essays quote bits.

Rejections are charged. Drawing a uniform integer below a kk that is not a power of two takes log2k\lceil\log_2 k\rceil bits and starts again if the value is out of range, and the discarded attempts count. Two hundred draws below 3 cost 2.53 bits each, against the 1.58 bits of information such a draw carries. The charged number is what a generator actually delivers; the information-theoretic number is a lower bound on what a cleverer procedure could spend, and the gap between them is where a good deal of derandomisation lives.

The generator is the site’s, seeded. Every bit comes from the same xorshift32 that produces the site’s random inputs, from a stated seed, because a caption may only quote a number the reader can reproduce. What that costs is an essay of its own.

What each algorithm spends

Five consumers, swept from n=256n = 256 to n=32,768n = 32{,}768 and fitted against the site’s candidate classes exactly as a comparison count would be:

consumer class spread constant bits at n=32,768n = 32{,}768
skip list, one build nn 1.03 2.03 per key 65,719
treap, one build nn 1.00 32.00 per key 1,048,576
reservoir, Algorithm R nlognn\log n 1.14 1.23 633,612
reservoir, Algorithm L logn\log n 1.52 455 7,952
randomised selection none granted 253

The first four are ordinary fits and the fifth is not, and both halves of that are worth going through.

The treap’s spread is 1.000, which is what an exact quantity looks like. Every key draws one 32-bit priority and nothing else draws anything, so the count is 32n32n with no variance whatever. It is the cleanest fit on the site and it is cleanest because there is nothing random about how much randomness it uses.

The skip list’s constant is 2.03 and its expected value is exactly 2. One flip decides whether to promote; on a heads it flips again; the expected number of flips before a tails is two. The 0.03 is the truncation at the maximum level and finite-sample wobble, and the fact that a measured constant lands within 1.5% of a hand-derived one is the sort of agreement that makes the counter trustworthy for the cases where no hand derivation is available.

Algorithm R’s nlognn\log n is a class nobody would expect from the description. “Take a sample of sixteen” sounds like a constant amount of work per item and a constant amount of randomness in total. It is one draw per item, each costing about log2i\log_2 i bits, and the total grows faster than the stream does. The essay on reservoir sampling is about what that costs and what removes it.

The class the site could not grant

Randomised selection is the interesting row and it is interesting because the answer is no.

Quickselect with a random pivot makes about 2log2n2\log_2 n pivot draws over its run, each drawn from a range that shrinks geometrically, so each costs about log2\log_2 of the current range. The total is quadratic in the logarithm. Measured over thirty seeds at each of eight sizes, bits divided by (log2n)2(\log_2 n)^2 is flat to 1.136, with a constant of 1.131.

The site’s lib/fit.js offers six candidate classes — constant, logn\log n, nn, nlognn\log n, n1.5n^{1.5}, n2n^2 — and (logn)2(\log n)^2 is not among them. The flattest candidate it does offer is logn\log n at a spread of 1.98, which is above the 1.6 tolerance and is refused.

So the class is withdrawn, and that is a deliberate choice rather than a gap. Adding (logn)2(\log n)^2 to CLASSES would grant it — and would also change what bestClass returns for every other figure on the site, in order to describe one line on one of them. The blast radius is a poor trade for a naming convenience, so the measurement is reported by its own assertion, assertSelectionRandomnessIsLogSquared, which requires two things at once: that the ratio against (log2n)2(\log_2 n)^2 is flat, and that the nearest offered class is refused. Both halves are needed, because a quantity growing slowly enough looks constant over any range short enough.

This is the third claim this site has withdrawn for a reason other than being wrong. Bubble sort on nearly sorted input fits neither nn nor n2n^2; the hybrid sort’s fitted class changes when the measurement range is extended. This one is the first withdrawn because the vocabulary is too small rather than because the data is ambiguous, and it is recorded as such.

Random bits consumed against n, k = 16Coin flips charged at the point they are spent, on logarithmic axes, with the class each consumer declares fitted by the same ratio test the comparison counts get. Rejections are charged: a draw below a range that is not a power of two costs whatever the rejection loop actually spent, not the ⌈log₂ k⌉ it would cost if the arithmetic were free. At n = 16,384 the consumers drawn here span 231.4 to 293,682 bits.10³10⁴10010³10⁴10⁵nrandom bitsreservoir, Algorithm R — n log nreservoir, Algorithm L — log nrandomised selection — (log₂ n)², no classn from 256 to 16,384bits charged including rejections
Fig. 2 The three that are not simply linear. Algorithm R rises with n log n; Algorithm L is nearly flat because for a fixed sample the number of skips grows only logarithmically; and randomised selection’s line is drawn without a class beside it, because no candidate the site offers fits it. Its label says (log₂ n)², which is what the measurement supports and what the fitting machinery cannot express.

Where the four counters agree and where they do not

The site now measures four quantities, and the useful thing about having four is the disagreements. Set the randomised structures side by side at n=4,096n = 4{,}096:

structure comparisons per operation peak slots random bits per key
skip list, p=12p = \tfrac12 23.1 2.01 pointers 2.03
skip list, p=14p = \tfrac14 ~24 1.33 pointers 2.67
treap 13.1 mean depth 1 priority + 2 pointers 32.00

No column ranks them the same way. The quarter-coin skip list beats the half-coin one on space, ties it on comparisons and loses on randomness. The treap beats both on comparisons per lookup — a mean depth of 13 against about 23 comparisons — and loses on randomness by a factor of sixteen.

That is the two-counts theme with a fourth column added, and each addition has done the same thing: taken a set of algorithms that a single number ranked cleanly and revealed that the ranking was an artefact of only counting one thing. Comparisons ranked the sorts; cache misses reordered them; slots reordered them again; and randomness reorders the structures in this phase. There is no total order over algorithms, and each counter added is another demonstration of it.

One run is an anecdote, here more than anywhere

Randomised selection’s row also produced the phase’s clearest lesson about how to measure this quantity, and it is the opposite of the intuition.

A skip list’s bit count at n=4,096n = 4{,}096 is smooth in nn, because it is a sum of 4,096 independent draws and sums concentrate. Randomised selection’s is a sum of about twenty-four draws, and twenty-four is not many: a single run’s total wanders by tens of per cent from seed to seed. Fitted from one seed per size, the counts refuse every candidate class including (logn)2(\log n)^2. Fitted from thirty seeds per size, they are flat to 1.14.

So the number of trials needed to measure a randomness consumption is set by how many draws the algorithm makes, not by how large its input is. That is a slightly surprising rule and it is a useful one: the algorithms that need the most careful measurement here are the ones that use the least randomness.

The instrument checks itself

A counter nobody has verified is a counter that reports whatever it reports, and this one is checked in two directions before any figure uses it.

Against a case counted by hand. One bit(), one seven-bit draw, one 32-bit uniform and one draw below 4 is 1 + 7 + 32 + 2 = 42 bits, and the counter must say 42. It must also say two 32-bit words were pulled, because 42 bits needs two, and two draws requested. That is the same check assertCountsAreExact makes on the comparison counters, for the same reason: a counter off by a constant leaves every curve the right shape with the wrong constant, and the constant is what this site reports.

Against the property the draws are supposed to have. Sixty thousand draws below 3 must land within 3% of a third each, and they measure 0.82% off. Without that check a drift towards the cheap word() % k shortcut would go unnoticed — it is faster, it is not uniform, and every reservoir figure would keep drawing while the sampler quietly stopped being correct.

Both checks are in the site’s gate, and both would fail the build rather than the figure. That matters because a broken bit counter is invisible in the output: the lines still rise, the fits still fit, and the constants are simply wrong.

Three more readings, because a fitted class is a claim about a range and the range is a choice.

Random bits consumed against n, k = 16Coin flips charged at the point they are spent, on logarithmic axes, with the class each consumer declares fitted by the same ratio test the comparison counts get. Rejections are charged: a draw below a range that is not a power of two costs whatever the rejection loop actually spent, not the ⌈log₂ k⌉ it would cost if the arithmetic were free. At n = 65,536 the consumers drawn here span 131,224 to 1,359,964 bits.10³10⁴10³10⁴10⁵10⁶nrandom bitsskip list, one build — nreservoir, Algorithm R — n log nn from 512 to 65,536bits charged including rejections
Fig. 3 The skip list against Algorithm R over a longer sweep, to sixty-five thousand. Rejections are charged where they are spent — a draw below a range that is not a power of two costs whatever its rejection loop actually spent — and the two consumers span 131,224 to 1,359,964 bits at the right-hand end.

Shortening the sweep is the sharper test of a fitted class, because a shorter range gives the fit less to work with and a class that only appears over four doublings is not a class.

Random bits consumed against n, k = 16Coin flips charged at the point they are spent, on logarithmic axes, with the class each consumer declares fitted by the same ratio test the comparison counts get. Rejections are charged: a draw below a range that is not a power of two costs whatever the rejection loop actually spent, not the ⌈log₂ k⌉ it would cost if the arithmetic were free. At n = 4,096 the consumers drawn here span 5,504 to 62,178 bits.10³10³10⁴nrandom bitsskip list, one build — nreservoir, Algorithm R — n log nreservoir, Algorithm L — log nn from 256 to 4,096bits charged including rejections
Fig. 4 All three over a shorter range. The fitted classes are the same classes, which is the only evidence available that they are classes rather than local slopes.

And dropping the skip list rescales the plate, which matters here because the refusal below is about a curve that is nearly flat beside one that is not.

Random bits consumed against n, k = 16Coin flips charged at the point they are spent, on logarithmic axes, with the class each consumer declares fitted by the same ratio test the comparison counts get. Rejections are charged: a draw below a range that is not a power of two costs whatever the rejection loop actually spent, not the ⌈log₂ k⌉ it would cost if the arithmetic were free. At n = 16,384 the consumers drawn here span 7,068 to 293,682 bits.10³10⁴10⁴10⁵nrandom bitsreservoir, Algorithm R — n log nreservoir, Algorithm L — log nn from 256 to 16,384bits charged including rejections
Fig. 5 And the two reservoirs alone, where the vertical scale is not set by the skip list. Algorithm L is the one whose declared class the site could not grant, and this is the plate the refusal was read off.

What the fourth counter is for

Having a number is not self-justifying, and it is worth being specific about what having this one has changed.

It makes two identical algorithms distinguishable. Algorithm R and Algorithm L return samples from the same distribution, hold the same kk slots, make the same single pass, and spend 1,356,399 and 9,380 bits respectively at n=65,536n = 65{,}536. Without a randomness counter the second algorithm has no reason to exist. This is the direct parallel of the two merge sorts in the space phase — identical in every counter the site had, different by a factor of logn\log n in the one it added.

It makes derandomisation a measurable trade. Median-of-medians selection spends zero bits, which is the point of it, and pays 2.54 times the comparisons of the randomised version. “Zero” is a measurement here rather than an observation about the source code, and it is what makes the exchange rate — comparisons per bit saved — a number.

Comparisons per element to find the median, n = 4,001, 30 seedsFor each pivot rule: the range of comparisons per element over 30 independent runs (the bar), with the mean marked. randomised pivot averages 2.96 and ranges from 1.56 to 4.12 — a factor of 2.6 on identical data, decided entirely by the coins. median of medians averages 7.94, spends no randomness, and varies by 1.7%. What derandomisation buys is not a lower cost; it is a cost that does not move.randomised pivot164 random bits2.96 ±23%median of mediansno random bits7.94 ±2%0.04.38.6comparisons per element — bar is the range over seeds, tick is the mean30 seeds, n = 4,001, median2.68× the mean, 14× the spread
Fig. 6 The exchange rate, drawn. Randomised selection averages 2.96 comparisons per element over thirty seeds and spends 164 random bits per run; median-of-medians averages 7.94 and spends none. The bar is the range over seeds and the tick is the mean — and the second thing the randomness counter makes visible is that the deterministic rule’s range is a fifteenth the width of the randomised one’s.

It gives derandomisation results a scale to be read against. There is a whole body of theory about removing randomness from algorithms — pseudorandom generators that stretch a short seed, the method of conditional expectations, kk-wise independent families that need only klognk\log n bits instead of nn. Every one of those results is a statement about a bit count, and without a bit count they are statements about a quantity the reader has no feel for. Knowing that a treap spends 32 bits per key and a skip list spends 2 makes “this construction uses O(logn)O(\log n) random bits” a claim with a comparison behind it.

And it puts a price on the parameter choices. A skip list at p=14p = \tfrac14 is cheaper to search and holds a third fewer pointers than one at p=12p = \tfrac12, and it costs 2.67 bits per key against 2.00. Without the counter that setting is simply better; with it, the improvement has a price and the price is on an axis the other counters do not have.

The floor under a bit count

The other three counters on this site each have a floor beneath them — comparisons have log2n!\log_2 n!, and the space and miss counts have their own bounds — and it turns out this one does too, computable from the same kind of counting argument and available for three of the five consumers above.

The question is: how much randomness does the algorithm’s output distribution actually require? An algorithm that produces one of NN equally likely structures must consume at least log2N\log_2 N bits on average, because a procedure spending fewer bits has fewer than NN distinguishable executions and cannot reach every structure. That is the same pigeonhole the memory-state floors use, applied to executions rather than to states.

The skip list. Its randomness is one geometric draw per key: the level, promoted with probability a half. The entropy of a geometric distribution at p=12p = \tfrac12 is exactly 2 bits, so a skip list cannot be built with less than 2 bits per key, and it measures 2.03. The structure is at its floor, to within the truncation at the top level, and the two-flips-per-key implementation is not merely convenient — it is optimal, because the flips are exactly the information the structure needs and none of them is discarded.

The treap. Its priorities are used for one thing: the relative order of the keys, which is what decides the shape. There are n!n! orders, so the floor is log2n!\log_2 n! bits in total, or about log2n1.44\log_2 n - 1.44 per key — 13.6 bits per key at n=32,768n = 32{,}768 against the measured 32.00. The treap spends 2.35 times what its own output distribution requires, and the excess is the difference between drawing a number wide enough that ties are unlikely and drawing a permutation.

That reframes the skip-list-against-treap comparison in the table above. The factor of sixteen between 2.03 and 32.00 is not a fact about which structure is more random; it is two facts. The treap genuinely needs more randomness than the skip list — 13.6 bits per key against 2 — because it is choosing among n!n! shapes where the skip list is choosing nn independent small levels. And on top of that it is wasting a further factor of 2.35 by drawing a fixed-width priority.

The waste is removable in principle and awkward in practice, which is worth stating because it is the usual shape of such things. Drawing priorities as a random permutation would hit the floor and would require knowing nn in advance, which a structure built by insertion does not. Drawing them lazily, refining a priority only when a comparison needs more bits, would also hit it and would make every comparison a variable-length operation. The 32-bit draw is the price of not having to do either, and having the floor is what turns that from a design detail into a measured overhead.

And the reservoir samplers are the case where the floor is most damning. A sample of kk items from a stream of nn is one of (nk)\binom{n}{k} subsets, so the floor is log2(nk)\log_2\binom{n}{k} bits — at n=65,536n = 65{,}536 and k=16k = 16 that is about 220 bits. Algorithm R spends 1,356,399 and Algorithm L spends 9,380. The better algorithm is a factor of 145 over the floor and the worse one a factor of six thousand, which says that the gap between them is real and that neither is anywhere near what the problem requires.

None of the three floors is achieved by anything anybody would deploy, and that is the ordinary situation for a floor. What having them changes is the reading of the table: a bit count is now a distance from a bound rather than a bare number, and 2.03 against a floor of 2 is a different kind of fact from 32.00 against a floor of 13.6.

What the counter does not measure

Three limits, all of which are the same limit seen from different angles: a bit count is not a cost.

It is not time. Pulling thirty-two bits from a xorshift generator costs a handful of cycles; pulling them from a cryptographic generator costs considerably more; pulling them from /dev/random may block. The bit count is machine-independent in exactly the way the comparison count is, and it is not a duration for exactly the same reason.

It is not entropy. The site’s generator is a 32-bit xorshift with a 32-bit state, so the total entropy available from one seed is 32 bits regardless of how many bits are drawn from it. A count of 1.36 million bits is a count of generator output, not of unpredictability, and conflating the two is the error the whole of the seeding essay is about.

And it is not a lower bound. The charged count is what the procedure spent, including rejections. A cleverer implementation of the same algorithm — buffering leftover bits across draws, or using an arithmetic-coding style extractor — spends less for the same distribution. So a comparison between two algorithms by bit count is only fair when both are implemented against the same drawing primitives, which they are here, and it is stated because it would not be true of numbers taken from two different libraries.

That last limit is the one worth carrying, because it is the honest version of what the counter is: a count of what this implementation asked the generator for, under a stated cost model, comparable across the algorithms in this library and nowhere else. Which is exactly what a comparison count is, and exactly what a slot count is, and is why all four sit on the same axes.

What this makes readable

Essays that name this one as a prerequisite.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

What links here

The 8 essays that link to this one and share the most of its objects, of 12 that link here.

The objects this essay names

Each one links to every other essay that touches it.

CacheComparison countComplexity classCost modelDerandomisationRandom bitsResource accountingSkip listTreap