Structures

The runs a permutation does not leave

A run of length L costs 2⌊log₂ L⌋ + 1 bits and replaces L, so coding runs pays above a mean run of six. This grid's mean run is 2.34, chance gives 1.90, and coding its runs makes it 17.8% larger.

The deferral said it in one clause: a permutation with structure has bit vectors with runs, and the plain vectors here charge one bit per bit.

This essay builds the coding that clause asks for. It makes the grid 17.8% larger, and the reason is arithmetic that could have been done first.

The runs a permutation leaves, against the runs chance leavesThe deferral this was built to answer said that a permutation with structure has bit vectors with runs. It has, and barely: the mean run over the parse's grid is 2.34 against 1.90 for a uniform permutation of the same size — a factor of 1.23, where a run coder needs about 6 to break even. The tree reorders its points by value at every level, which is what destroys the locality the two orderings shared, and the bottom levels are indistinguishable from a coin.024601234567891011level of the wavelet treemean runthe parse's grida uniform permutationbreak-even3,612 points · 12 levels1.23x chance
Fig. 1 The mean run at each level of the grid, against a uniform permutation of the same size, with the length a run coder needs to break even marked.

What coding a run costs

A bit vector is a sequence of alternating runs. Store their lengths and one bit saying what the first run holds, and the vector is recoverable.

A length is a positive integer of unbounded size, so it needs a self-delimiting code. Elias gamma is the standard one: write ⌊log₂ L⌋ zeros, then L in binary, for 2⌊log₂ L⌋ + 1 bits.

That is 1 bit for a run of 1, 3 for a run of 2 or 3, 5 for a run of 4 to 7, 7 for 8 to 15. Set against the L bits the run replaces:

run costs replaces
1 1 1
2 3 2
3 3 3
4 5 4
6 5 6
7 5 7

The coding first breaks even at six. Below that it costs more bits than the run it replaces, and a vector of short runs comes out larger.

What a run costs to write down, against what it replacesA run of length L is coded in 2⌊log₂ L⌋ + 1 bits and stands in for L bits of vector, so the coding is a saving only where the second line is above the first. They cross at L = 6: a run of 6 costs 5 bits and replaces 6. Below that every run costs more than it saves, and the mean run on this grid is 2.34 — which is why coding its runs makes it larger. The directory is not in this picture and does not shrink at all.05101551015length of the runbitsthis grid's mean run, 2.34what it replaceswhat it costsgamma codingbreak-even at 6
Fig. 2 What a run costs to write down against what it stands in for, with this grid’s mean run marked. Everything to the left of the crossing is a loss.

It is worth being clear that gamma is not a bad code. It is within a constant factor of optimal for an integer of unknown magnitude, and any self-delimiting code has the same shape — a prefix saying how long the number is, then the number. A code that spent one bit on a run of one would have to spend more somewhere else.

What the table above says is not that this code is inefficient. It is that coding runs is the wrong idea for a vector whose runs are short, whatever code is used, because the length is the information and a short length is almost as much information as the bits it describes.

What the grid’s runs actually are

Measured over the twelve levels: mean run 2.34.

A uniform permutation of the same size gives 1.90. So the parse’s grid is more ordered than chance, by a factor of 1.23 — the deferral was right that the structure is there. It is a fifth of the way to being worth coding.

The distribution across levels is worth reading. The top level is 2.05, the middle levels reach 3.19, and the bottom two are 1.65 and 1.33. The last of those is very close to the 1.25 a fair coin gives, and there are 3,612 bits of it.

That distribution is itself the answer to whether a per-level choice helps, and the level where compression stops paying is the essay that measures it. The short version is visible already: the best level here is at 3.19, which is still below break-even, so no level of this grid is worth run-coding.

Why the middle is the longest

The shape of the curve is not what the argument predicts, and it is worth stopping on.

One would expect the top level to be the most ordered — it is the one where x and y are still in their original correlation — and every level below it to be worse, as the stable reordering destroys locality.

What happens instead is that level zero is 2.05 and levels four to six are around 3. The reason is that the top level’s split is by value, at the midpoint of the whole range, and a boundary’s y rank is only loosely tied to its x rank: adjacent boundaries often fall on opposite sides of a midpoint that is far from either of them. Further down, a node’s value range is narrow and the points in it are a set of boundaries that genuinely resemble each other, so consecutive ones do fall the same way.

The effect then collapses at the bottom, where a node has two points and there is nothing left to resemble.

Where each coding pays, level by levelEvery level holds the same 3,612 bits of payload, so the plain vector is a flat line at 4,300. The block-coded vector saves 6.3% at the top and -0.3% at the bottom; the run-coded one costs 23.2% more at the top and 52.5% more at the bottom, because a run of two costs three bits and replaces two. Choosing the best coding for each level separately comes to 47,642 bits against 47,668 for the best single choice — a saving of 0.1%, which is not worth the apparatus of choosing.02e+34e+36e+301234567891011level of the wavelet treebitsplainblock-codedrun-coded3,612 points · 12 levels24 copies of 2048 characters
Fig. 3 What that does to the sizes. The run-coded line is worst at the top and the bottom, and its best levels are still above the plain vector.

The number chance gives

It is worth deriving 1.90 rather than only measuring it, because a control one can predict is a control one can trust.

A vector of independent fair bits has a run ending at each position with probability one half, so its runs are geometric with mean two. A permutation’s level is not independent bits — it has exactly as many ones as zeros — and sampling without replacement makes runs very slightly shorter than two. At 3,612 bits that correction is small, and the measured 1.90 is a little below two for exactly that reason.

So the number to beat is two, near enough, and the grid gives 2.34. Everything in this essay follows from those two numbers and from the break-even of six.

The measurement

Plain: 51,600 bits. Run-coded: 60,805.

Broken into parts, the coding does what it says on the payload: 43,344 bits of plain payload become 45,973 — which is already a loss, because the mean run is below break-even — and the directory goes from 8,256 to 14,832.

The directory is the larger half of the damage, and it is worth saying why it grows — the same asymmetry a bit for every bit records for the plain vector, working the other way. A plain vector’s directory is a counter every 64 bits; a run vector’s is a sampled entry every 32 runs, and at a mean run of 2.34 that is an entry every 75 bits. More samples, each holding a position and a count.

The part that shrinks, and the part that does notPayload and directory, for each coding of the same grid. The run-coded vector's payload is 45,973 bits — genuinely smaller than the plain vector's 43,344 — and its directory is 14,832, which is 24.4% of what it actually costs. A number quoted without it reports a structure 1.32x smaller than the one being queried, and that is the shape of every over-claimed compression number in this collection.plain51,60043,344 payload · 8,256 directoryblock-coded47,66833,508 payload · 14,160 directoryrun-coded60,80545,973 payload · 14,832 directorybits · solid is the payload3,612 points24.4% directory
Fig. 4 Payload and directory for each coding. The run vector loses on both, and the directory is the bigger of the two losses.

Where the 17.8% actually came from

The argument so far is a mean against a break-even: 2.34 against 6, therefore a loss. That argument predicts the sign correctly and its magnitude is wrong by a factor of eight, and the discrepancy is worth chasing because it says which of the two numbers above is load-bearing.

Take the break-even arithmetic at the mean. A run of 2.34 costs 2log22.34+1=32\lfloor\log_2 2.34\rfloor + 1 = 3 bits and replaces 2.34, so the payload should come out 1.47 times larger. It comes out 1.061 times larger — 45,973 against 43,344.

The gap is the distribution. Gamma’s cost grows as the logarithm of a run’s length while the bits it replaces grow linearly, so evaluating a concave cost at the mean overstates what the coder actually spends: a vector whose runs are mostly of length one, with a tail of longer ones, pays one bit for each of the cheap majority and the mean never sees it. The mean run is the wrong statistic for the payload, in exactly the way expected is not average sets out — a single number standing in for a distribution, in a place where the function applied to it is not linear.

So the payload loses six per cent, not forty-seven, and the headline is 17.8. The rest is the directory, and the directory is where the mean run is exactly the right statistic.

A run vector’s directory samples every 32 runs, so its size is the run count divided by 32, and the run count is n/μn/\mu. That is linear in 1/μ1/\mu with no concavity to soften it, and at μ=2.34\mu = 2.34 the structure is sampling every 75 bits where a plain vector’s counter sits every 64. The directory goes from 8,256 bits to 14,832 — an increase of 6,576 against the payload’s 2,629, so the directory is 71 per cent of the damage and the coding of the runs themselves is a quarter of it.

Two things follow, and neither is visible from the break-even table alone.

The first is that the hybrid sketched below would recover less than it looks like it should. Storing short stretches verbatim fixes the payload, which was never the expensive half; unless it also reduces the number of entries the directory has to sample, the 6,576 bits stay. A coding that eliminated the payload loss entirely would still leave this grid about twelve per cent larger than the plain vector.

The second is a correction to how this measurement should be read against a bit for every bit. That essay’s finding is that a rank directory is a fifth of a plain bit vector and is the part nobody counts. Here it is a quarter of the run vector and three quarters of its loss. The technique changed, the payload changed, and the part that decided the outcome was the same part both times.

The shuffle, which is the control

The claim under test is that the ordering carries information. The way to check it is to destroy the ordering and see whether the number moves.

Shuffle the top level’s bits. Its length is unchanged, its number of ones is unchanged, and its zero-order entropy is unchanged to the last bit. Its runs are destroyed.

Run-coded, the ordered vector is 5,296 bits and the shuffled one is 5,369: a difference of 1.4%.

That is the finding. Whatever ordering the parse’s grid has, a run coder can extract 1.4% of a vector’s worth of it — and it charges 22% for the attempt.

Why the control is the right one

A control has to hold fixed everything except the property under test, and this one does: the shuffle preserves length, density, and therefore every zero-order statistic. What it changes is exactly the arrangement, which is what a run coder reads.

That makes it a sharper control than the uniform permutation a block, a class and an offset uses. A uniform permutation differs from the parse’s grid in its provenance; a shuffled level differs from its own unshuffled self in nothing but order.

A run is a property of the input is where this collection first insisted on the distinction, about a sort’s runs rather than a vector’s. The rule is the same: a measure that does not move when the thing it claims to measure is destroyed is measuring something else.

The whole grid, and the control that says how much of it is structureThree codings over 3,612 points, and the same three over a uniform permutation of the same size. The best coding saves 7.6% on the parse's grid and 2.6% on a permutation with no structure at all — so about 5.0% of the saving is the data and the rest is what block coding gets from any vector of this density. That difference is the honest answer to whether a compressed grid is worth building, and it is a few per cent of a structure that is itself a quarter of the index.plain51,600block-coded47,6687.6%run-coded60,805-17.8%plain, uniform51,600block-coded, uniform50,2422.6%run-coded, uniform67,121-30.1%bits · the lower three are a permutation with no structure3,612 points7.6% against 2.6%
Fig. 5 The three codings and their uniform control. The run coder is above the plain vector on both, which is what a coding that charges for absent structure looks like.

The one thing it is better at

There is a column where the run vector wins, and it is not size.

On the descent — ranks only, which is what every rectangle pays whatever its answer — the run vector inspects 100,156 bits over sixty rectangles against the plain vector’s 112,532. It is cheaper to read.

The reason is that a plain rank scans up to a block of bits and a run rank walks runs from a sampled position, and at a mean run of 2.34 with a sample every 32 runs the walk covers about 75 positions in 32 steps. Fewer steps than 64 bits.

That is a real property and it is a fragile one, because it is the only cost in this strand that follows the data. A plain vector’s rank scans a fixed block whatever the block holds; a run vector’s cost rises as the runs shorten. Measured across a divergence sweep: 13.9 bits inspected per rank at exact copies, 19.6 at five per cent changed, 22.3 at forty — a rise of 1.60, against the plain vector’s flat 24.8, 22.6, 21.3.

Only one of the three cares what the data isThe descent is ranks only — reporting a point costs a select per level, and a select is a different structure in all three vectors — so this is what every rectangle pays whatever its answer. The plain vector scans a fixed block and reads 24.8 to 21.3 bits a rank whatever the collection holds. The run vector walks runs, so as the copies diverge and the runs shorten its cost rises from 13.9 to 22.3. At this grid's own size the three are within a tenth of each other, so the whole difference between them is size — and only one of them has a cost that the data can move.01020012bits inspected a rank0%5%40%characters changed between copiesplainblock-codedrun-coded40 rectangles a point1.60x rise
Fig. 6 Bits inspected per rank, as the copies diverge. One line follows the data and two do not.

What a coding that degraded gracefully would look like

There is a design that would have avoided this outcome, and naming it is useful because it is what a careful implementation ships.

Code a run only when it is long enough to pay, and store short stretches verbatim. That is a hybrid — a flag bit per stretch, then either a gamma-coded length or a literal — and its size on a vector of short runs is the vector plus a flag, rather than the vector times 1.06.

It is not built here, and the reason is the strand’s comparison. Three codings measured on one structure, each in its published form, is a comparison a reader can check against other people’s numbers. A hybrid tuned until it does not lose is a fourth thing, and its result would be “a coding that was designed not to lose does not lose”.

What it would be worth is a deferral, and the arithmetic says it is worth approximately nothing: a hybrid on this grid would land within a per cent of the plain vector, which is what “does not lose” means here.

What it comes to, on the index the grids are bolted toThe whole phrase index over 49,152 characters, with each coding in both of its grids. The grids are 28.1% of the structure to begin with, so the best saving available — 20.6% of them — is 5.8% of the index. All three answer the same 261 occurrences in the same 24,951 rank and select operations, because the descent does not know what its vectors are made of.plain366,648102,972 of it the two gridsblock-coded345,47581,799 of it the two gridsrun-coded359,27495,598 of it the two gridsbits · solid is the grids49,152 characters · z = 36125.8% off the index
Fig. 7 The stakes, one last time. The grids are a quarter of the index, so even a coding that took everything available would move the structure by a few per cent.

Where run coding does work

It would be wrong to leave this reading as a verdict on the technique, because this collection has a structure where the same coding is the whole point.

The index that stores the runs is a Burrows-Wheeler transform kept as its runs — the measure a floor under a run count puts an exact floor beneath — and there the mean run on a repetitive collection is in the tens or hundreds — a transform of eight copies of a text has runs proportional to the base text rather than to the collection. That is far above the break-even, and the structure is smaller than the text by a large factor.

The difference is what is being coded. A transform’s runs come from repeated contexts, which repetition creates directly. A wavelet tree level’s runs over a permutation come from a correlation between two orderings, filtered through a splitting rule that reorders the points at every level.

The same coding, applied to two structures, differs by two orders of magnitude in what it is worth. Repetition is not entropy is the theme; this is the sharpest pair of instances in it.

What could have been computed first

The break-even is six. The mean run needed to be known.

Measuring a mean run is one linear pass over a vector and costs nothing, and it would have settled the question before any coding was written: 2.34 against a break-even of 6, on a structure whose payload is 16% above the information-theoretic floor for any permutation at all.

This collection’s habit is to build the thing rather than argue about it, and that habit is right more often than not — the table the links already knew records a case where building it overturned a published conclusion. It is worth recording the other side too: a one-pass measurement and a two-line arithmetic would have produced this essay’s answer without the structure, and the structure’s value is that the answer is now a number rather than an expectation.

What the deferral was actually seeing

It is worth reconstructing the intuition once more, because it was not foolish and the failure is specific.

A repetitive collection’s phrase boundaries genuinely do cluster. Boundaries that come from the same region of the base text sit near each other in the prefix ordering and near each other in the suffix ordering, so the permutation has patches where it is nearly the identity. Plotted as points, the grid looks like a thickened diagonal with noise, and a thickened diagonal is exactly the shape whose top-level bit vector is a long run of zeros followed by a long run of ones.

The top level’s mean run is 2.05.

The gap between the picture and the number is the thickness. A diagonal band of width w produces, at level zero, alternating stretches whose length is about the number of consecutive x positions that stay on one side of the midpoint — and with a band even a few per cent of the range wide, that is a handful of positions. A picture that looks like a line is a vector that looks like noise, and there is no contradiction between the two.

What is checked, and what must fail

Rank, select-one and select-zero agree with a scan on a deliberately clustered vector — clustered, because the grid’s own levels have almost no runs and a check on them would exercise none of this machinery.

Three things must fail, and the third is the one this essay depends on.

A directory that samples positions rather than ones leaves rank right and select wrong: 287 of 287 selects wrong, and 309 ranks wrong once the counts are gone. A size that omits the directory reports the structure 1.32 times smaller than it is.

And a run vector that forgets which value its first run holds is one bit, produces exactly the complement of the right answer, and reports an identical size: 324 of 334 ranks wrong, with the payload unchanged to the bit. That is the check that guarantees the sizes above are sizes of a working structure.

The runs a permutation leaves, against the runs chance leavesThe deferral this was built to answer said that a permutation with structure has bit vectors with runs. It has, and barely: the mean run over the parse's grid is 2.02 against 1.94 for a uniform permutation of the same size — a factor of 1.04, where a run coder needs about 6 to break even. The tree reorders its points by value at every level, which is what destroys the locality the two orderings shared, and the bottom levels are indistinguishable from a coin.0246012345678910111213level of the wavelet treemean runthe parse's grida uniform permutationbreak-even13,140 points · 14 levels1.04x chance
Fig. 8 The same measurement on a collection nearly four times as large. The runs are shorter, chance is the same, and the ratio between them narrows.

What this leaves for the last essay

Two codings measured, one worth 7.6% and one worth −17.8%, and a per-level plate showing that neither is best everywhere.

That invites the obvious question — choose the best coding for each level — and the level where compression stops paying is what that is worth. The answer is a number small enough to end the strand.

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

Every essay whose body links to this one.

The objects this essay names

Each one links to every other essay that touches it.

Bit vectorBreak-evenCompressionControlGamma codingGridMeasurementPermutationRank directoryRun-length encodingSpace overheadWavelet tree