The data that is not a number

Six bits a class

Coding a bidirectional index's wavelet-tree levels in blocks left a part that did not shrink: six bits a block for the block's class, its count of ones, which on English words became a quarter of the levels. Coding the classes by their runs was predicted to halve them. It takes a third off, 0.480 bits a character to 0.323, because two thirds of the blocks are uniform but they come in stretches of about two. One entropy code over the classes does about as well, and on the repetitive collection better. Runs also cut the reads a rank makes by 15%. On random letters runs make the classes larger, and the pointer a variable-length stream needs at every superblock eats most of what an entropy code saves.

The part every saving left alone coded every level of a bidirectional index’s wavelet trees in blocks of 63 bits. Each block became its class, the number of ones it holds, in six bits, and its offset, which of the arrangements with that many ones it is. On English words that took the levels from nearly six bits a character to under two, because the Burrows–Wheeler transform groups letters and a level’s blocks are often all zeros or all ones. Once the offsets had shrunk, the classes had not. They are six bits for every block whatever the block holds, and on English words, with the superblock counts beside them, they had become a quarter of the levels.

Its closing section proposed coding the class sequence itself. On a text whose blocks are mostly uniform, most classes are 0 or 63, and runs of one class value could be stored as the value and a length. A pointer into the run stream at every superblock would let a rank still find its place. The prediction was that on English words and the repetitive collection the classes would fall by more than half, taking the levels from 1.90 bits a character to about 1.5. On random letters, whose classes are spread, nothing would be saved.

Three ways to hold the classes

The index and its offsets are the earlier page’s, untouched. The superblock counts and offset pointers stay as they were. Only the classes change, three ways. Six bits a class, as built: a rank finds block j’s class at a known position and sums the classes before it in its superblock. One entropy code: a single Huffman code over the class values of every level in the index, its table stored once. Runs of a class: the class sequence as runs, each run its value in six bits and its length in an Elias-gamma code, which the runs a permutation does not leave priced for a grid’s bits.

Both variable-length codings lose something the fixed one has: a class can no longer be found by its position. A rank must start where its superblock’s classes begin and decode forward. So each superblock gets one more entry, a pointer into the class stream, and it is charged to both. The texts are the earlier page’s four, 16,384 characters each: DNA and protein letters at random, English words and a repetitive collection. Sizes are bits a character, averaged over the two halves. Reads are vector entries read an extension of 64 exact backward searches of eight characters.

A third off, not half

Coding the classes too: on English words six bits a class cost 0.480 bits a character, one entropy code over every level's classes 0.344, and runs of a class 0.323 — a third off, not the half predicted; on the repetitive collection 0.480, 0.266 and 0.295; on random protein runs cost more than six bits a class, 0.535 against 0.481Bits a character, averaged over the two halves, held by the classes of every coded level, including the pointers into the class stream at every superblock that the variable-length codings need. DNA: six bits 0.287, entropy code 0.277, runs 0.258. Protein: six bits 0.481, entropy code 0.461, runs 0.535. English: six bits 0.480, entropy code 0.344, runs 0.323. Repetitive: six bits 0.480, entropy code 0.266, runs 0.295.six bits a classone entropy coderuns of a class0.2870.2770.258DNA0.4810.4610.535protein0.4800.3440.323English0.4800.2660.295repetitivebits a character, classes of both trees, per halfclasses, with their pointers16,384 characters a text
Fig. 1 The classes’ bits a character, pointers included. DNA: 0.287 at six bits a class, 0.277 entropy-coded, 0.258 as runs. Protein: 0.481, 0.461, 0.535. English: 0.480, 0.344, 0.323. Repetitive: 0.480, 0.266, 0.295.

On English words the classes fall from 0.480 bits a character to 0.323 as runs and 0.344 under one entropy code: a third off, not the half predicted. The repetitive collection does better under the entropy code, 0.266, which is the half the prediction asked for, and less well as runs, 0.295. On random protein the entropy code saves a little, 0.481 to 0.461, and runs make the classes larger, 0.535. On random DNA both save a little.

The prediction’s premise was right about the values and wrong about their arrangement. Most classes on English words are 0 or 63, and that is what the entropy code uses. They do not come in long runs, and runs were what the proposal meant to use.

Uniform blocks, in short stretches

Why the runs of a class are short: on English words 45% of the blocks are all zeros and 21% all ones, so two thirds of the classes are one of two values — but uniform blocks of zeros, uniform blocks of ones and mixed blocks interleave as the transform's runs of one letter begin and end, and a run of one class lasts 2.17 blocks on averageThe share of 63-bit blocks in every coded level of both trees that are all zeros, all ones, or mixed, and the mean length of a run of one class value. DNA: 17% zeros, 8% ones, 75% mixed, mean run 1.46 blocks, class entropy 4.93 bits. Protein: 7% zeros, 1% ones, 92% mixed, mean run 1.16 blocks, class entropy 5.01 bits. English: 45% zeros, 21% ones, 34% mixed, mean run 2.17 blocks, class entropy 3.51 bits. Repetitive: 54% zeros, 24% ones, 22% mixed, mean run 2.50 blocks, class entropy 2.56 bits.all zerosall onesmixedDNArun 1.46 · H 4.93proteinrun 1.16 · H 5.01Englishrun 2.17 · H 3.51repetitiverun 2.50 · H 2.56blocks of 63 bits, both treesH: entropy of the class values
Fig. 2 The share of blocks in every coded level that are all zeros, all ones, or mixed, and the mean run of one class. DNA: 17%, 8%, 75%, runs of 1.46 blocks. Protein: 7%, 1%, 92%, 1.16. English: 45%, 21%, 34%, 2.17. Repetitive: 54%, 24%, 22%, 2.50.

On English words 45% of the blocks are all zeros and 21% all ones, two thirds uniform, yet a run of one class value lasts 2.17 blocks on average. A block of 63 bits in one level is uniform when 63 consecutive characters of the transform all go the same way at that level’s split. The transform’s runs of one letter are often that long on English words, and often not. A stretch of the transform where one letter repeats gives a few uniform blocks, then a block that straddles the change of letter is mixed, then the next letter’s uniform blocks may be zeros or ones. The uniform blocks are common, and they are broken up.

The bits underneath say the same thing more exactly. In the forward tree on English words, a level’s stretches of one bit value of at least 63 bits — the only ones that can make a block uniform — number 189, average 340 bits and hold 78.5% of all the levels’ bits. A stretch of 340 bits covers five or six blocks, but its first and last blocks usually straddle its ends and are mixed, so it yields a run of about four uniform blocks. Between those runs sit the mixed blocks, a third of all blocks, whose classes are whatever counts of ones they happen to hold and rarely equal their neighbours’. Each of those is a run of one. The mean of 2.17 is long uniform runs averaged with many runs of a single mixed block.

The transform’s own runs are shorter still. Its runs of one letter on English words average 3.43 characters, and a level’s bit runs are longer only because a level merges letters on the same side of its split: a run of the bit 0 at the top level is any stretch of letters from the alphabet’s first half. The transform that emits nothing measured the clustering as runs of letters. The levels see it as runs of halves, quarters and eighths of the alphabet, and a block of 63 bits sees it at a resolution of 63 characters.

A run code pays six bits for the value and a gamma length for every run. A run of one block costs seven bits, more than the six it replaces. A run of two costs nine where twelve were spent, and a run of four costs eleven against twenty-four. At a mean of 2.17 blocks the saving is about a third, which is what was measured. The prediction assumed runs as long as the transform’s own, and a class run is a run of 63-character blocks, which is a much coarser thing.

A block, a class and an offset found the same kind of distinction on a grid. Its blocks’ classes were spread, and the coding saved there what the spread of the bits allowed. Here the bits are grouped, and the question is whether the grouping survives being cut into blocks of 63. On English words it partly does.

The class values themselves

Why one entropy code helps even on random letters: a block's count of ones is never spread evenly over 0 to 63 — on random protein it piles up in the middle, an entropy of 5.01 bits where six are spent, and on English words it piles up at both ends, 3.51 bits — but on random protein the pointer each superblock needs into a variable-length class stream eats most of what the entropy code savesThe share of 63-bit blocks in the coded levels of both trees with each number of ones, for random protein text and English words. Random protein: 7% with none, 1% with all, entropy 5.01 bits. English words: 45% with none, 20% with all, entropy 3.51 bits.00.2000.4000816243240485663ones in a block of 63 bits (the class)share of blocksrandom proteinEnglish wordsboth trees, every levelpartial last blocks counted by their ones
Fig. 3 The share of blocks with each number of ones. Random protein: piled in the middle, 7% with none, entropy 5.01 bits. English words: piled at both ends, 45% with none and 20% with all 63, entropy 3.51 bits.

On random protein the classes pile up in the middle, and their entropy is 5.01 bits where six are spent. On English words they pile up at both ends, and their entropy is 3.51. No coding of a fixed width can be right for both. Six bits allows for every count from 0 to 63 equally, and no text produces that. A random block’s count of ones clusters around its mean like any sum of coin flips, so even random letters leave a bit a class on the table. English words leave two and a half.

The entropy code reads those distributions directly, which is why it does well on the repetitive collection, the most bimodal of the four, and why it saves something on protein where runs lose. Runs read a different property, how the values follow one another. On English words the two properties are worth about the same. On the repetitive collection the distribution is worth more. On random letters only the distribution is worth anything.

Random DNA sits between. Its balanced tree has three levels for four letters and the end marker. The last letter, t, gets a code whose second and third bits are always zero, so the two nodes below it hold nothing but zeros, and the end marker, which occurs once, shares a node with a, so that node is nearly all ones. Those three nodes are DNA’s 17% all-zero blocks and 8% all-one, and both codings find something to save, 0.010 bits a character under the entropy code and 0.029 as runs. It is the same waste of the balanced shape that the earlier page found the block code taking back, seen again in the classes: a node that says almost nothing still has a class for every block, and coding the classes is what finally makes it cost almost nothing.

What the pointers cost

What a variable-length class stream pays to stay searchable: a rank must find where its superblock's classes begin, so each superblock needs a pointer into the stream — 0.039 bits a character on random protein, where the entropy code's own bits are 0.422 against six bits a class at 0.481, and 0.036 on English words beside 0.308 — the pointers turn a protein saving of 12% into 4%Bits a character of the classes under each variable-length coding, split into code bits and the superblock pointers into them, with six bits a class (tick) for comparison. DNA, entropy code: code 0.255, pointers 0.022, against 0.287; DNA, runs: code 0.238, pointers 0.020, against 0.287; Protein, entropy code: code 0.422, pointers 0.039, against 0.481; Protein, runs: code 0.496, pointers 0.040, against 0.481; English, entropy code: code 0.308, pointers 0.036, against 0.480; English, runs: code 0.286, pointers 0.036, against 0.480; Repetitive, entropy code: code 0.229, pointers 0.036, against 0.480; Repetitive, runs: code 0.258, pointers 0.037, against 0.480.code bitspointers into itDNA, entropy code0.277DNA, runs0.258protein, entropy code0.461protein, runs0.535English, entropy code0.344English, runs0.323repetitive, entropy code0.266repetitive, runs0.295tick: six bits a classbits a character, per half
Fig. 4 Bits a character of the classes, split into code and superblock pointers. Protein, entropy code: 0.422 and 0.039, against 0.481 at six bits a class. English, entropy code: 0.308 and 0.036. English, runs: 0.286 and 0.036. Repetitive, entropy code: 0.229 and 0.036.

On random protein the entropy code’s own bits are 0.422 a character against six bits a class at 0.481, a saving of 12%. The pointers it needs, 0.039, take that to 4%. A pointer is the position of a superblock’s classes in the stream, about twelve bits for every 32 blocks, and it is the price of decoding forward instead of indexing. On English words and the repetitive collection the entropy code saves enough for the pointers not to matter. On random letters they consume most of the saving, and an implementer would reasonably keep six bits a class there.

This is the same kind of cost the structure paid for before the first query found in a grid’s rank directory: bits that answer nothing on their own and exist so that a query can start in the middle. Every variable-length coding of a sequence that must be searched pays some version of it. The fixed-width classes paid none because their position was their pointer.

What a rank reads

Runs pay back in reads as well as bits: a rank sums the ones in every block before its own in its superblock, and walking runs of one class instead of single classes reads 365 entries an extension on English words against 429, 15% fewer; on random protein, where runs are a block long, 408 against 424Vector reads an extension of 64 exact searches of eight characters through both halves, with the classes walked one by one or run by run. DNA: classes 253.09, runs 229.62. Protein: classes 423.98, runs 408.35. English: classes 429.11, runs 364.86. Repetitive: classes 418.89, runs 344.56. An entropy-coded class stream is walked class by class, as the six-bit one is.walking classeswalking runsDNA253230protein424408English429365repetitive419345vector reads an extensionsuperblocks of 32 blocks
Fig. 5 Vector reads an extension, walking classes or runs. DNA: 253 and 230. Protein: 424 and 408. English: 429 and 365. Repetitive: 419 and 345.

Walking runs instead of classes, an extension on English words reads 365 vector entries against 429, 15% fewer. A rank sums the ones in every block before its own in its superblock. With fixed classes it reads each of those classes, sixteen on average. With runs it reads each run, and a run of three blocks is one read. So the coding that saves bits on English words also saves reads, which is rarer than it sounds: the earlier page’s block coding cost 1.4 times the reads of the plain levels for its saving.

An entropy-coded class stream saves no reads. Its classes are still read one at a time, and decoding a variable-length code is, if anything, slower per class than reading six bits at a known offset. So on English words the two codings that cost about the same bits differ in what they do to a search. Runs save 15% of the reads, and the entropy code nothing. On the repetitive collection, where the entropy code is 0.03 bits a character smaller, runs still save 18% of the reads, and the choice depends on which currency the index is short of.

Bits and steps on one frame set the rule that a size saving is drawn beside its cost in operations. Here, for once, the two move the same way on the texts where either moves at all.

The levels as a whole

What the class coding does to the levels as a whole: on English words they go from 1.877 bits a character to 1.720 with runs, a twelfth off, where about 1.5 was predicted; on the repetitive collection from 1.068 to 0.853 with one entropy code — against plain levels of nearly six bits a character on every text (dashed)Bits a character, averaged over the two halves, held by the coded levels — offsets, classes, superblock counts and pointers — with the classes in six bits, one entropy code, or runs. DNA: 2.225, 2.215, 2.197; plain levels 3.565. Protein: 4.676, 4.656, 4.730; plain levels 5.970. English: 1.877, 1.741, 1.720; plain levels 5.967. Repetitive: 1.068, 0.853, 0.883; plain levels 5.971.six bits a classone entropy coderuns of a class2.232.222.20DNA4.684.664.73protein1.881.741.72English1.070.850.88repetitivebits a character, levels of both trees, per halfdashed: plain levelsoffsets and directory included
Fig. 6 Bits a character of the coded levels, everything included. DNA: 2.225, 2.215, 2.197. Protein: 4.676, 4.656, 4.730. English: 1.877, 1.741, 1.720. Repetitive: 1.068, 0.853, 0.883. Plain levels: about 3.6 on DNA and 6.0 on the other three.

On English words the levels go from 1.877 bits a character to 1.720 with runs, a twelfth off, where about 1.5 was predicted. The classes were a quarter of the levels, and a third off a quarter is a twelfth. On the repetitive collection the entropy code takes the levels from 1.068 to 0.853, a fifth off, because there the classes were nearly half of what remained.

Against the plain levels these are refinements. The block coding took English words from 5.97 bits a character to 1.88, and the class coding takes another 0.16. What it changes is the shape of what is left. The offsets are now 1.40 bits a character on English words, the classes 0.32 and the superblock counts and pointers the rest. The offsets are the part that reflects the text, and the next saving has to come from them. The level where compression stops paying found the point on a grid where further coding cost more than it saved. This index is near that point for its directory, and still some way from it for its offsets.

Which coding, for which text

The four texts give three different answers. On random letters, keep six bits a class. The entropy code’s saving there is real but the pointers take most of it, and runs make the classes larger because a mixed block’s class almost never repeats. On English words, use runs: they save the most bits, 0.157 a character against the entropy code’s 0.136, and they are the only coding that also saves reads. On the repetitive collection the entropy code saves more bits, 0.214 a character against runs’ 0.185, and runs save the reads, so the answer depends on which the index is short of.

None of this can be decided without looking at the text, and all of it can be decided by looking at the class sequence once, at build time. A builder that counted the class values and their runs for each level could choose per level. A level whose classes are spread keeps six bits, a level of long uniform stretches takes runs, and a bimodal level without long stretches takes the entropy code. The measurements here chose one coding for the whole index, and a choice per level would do at least as well. A code word is at least one bit is one reason the choice is not obvious even per level: an entropy code cannot spend under a bit on a class, however common, so on a level that is almost all one class the run code wins by a margin the class distribution alone does not show.

The limits of the measurement

One block length, one superblock. Blocks of 63 bits and superblocks of 32 blocks are the earlier page’s. With longer blocks a uniform stretch is fewer, longer classes. With shorter ones there are more classes and the class coding matters more. Neither was swept.

One code for every level. The entropy code is shared by all the levels of both trees. Levels differ: the top level of a balanced tree over English words is biased differently from its bottom level. A code per level would fit better and cost a table each, which on these sizes was found to cost more than it saved.

Two pointers where one might do. Each superblock already holds a pointer into the offsets, and the variable-length class stream adds a second. A layout that stored each superblock’s classes and offsets together, classes first, would need only one pointer, and on random protein that would recover most of the 0.039 bits a character the second pointer costs. It was not built.

Reads, not time. Decoding a gamma length or a Huffman code word costs more per entry than reading six bits at a fixed position, and the reads count every entry the same.

Still open: the offsets, read as a stream

With the classes coded, the offsets are what is left of the levels on English words: 1.40 bits a character out of 1.72. An offset says which arrangement of its class a block is, and for a uniform block it is empty. For a mixed block it is ⌈log₂ C(63, c)⌉ bits, as if every arrangement were equally likely. On English words a mixed block usually straddles a change of letter, and its ones come in one or two stretches, not scattered. Those arrangements are a small fraction of all arrangements with that many ones.

The measurement that follows codes a mixed block’s offset as the positions where its bits change, a gamma code for each, instead of the index of its arrangement, and counts bits and reads on the four texts. The prediction is that on English words the offsets fall by about a third, because most mixed blocks change value only once or twice. On random letters they should grow, since a random block changes value about thirty times and its arrangement index is the shortest description it has. The question is whether a coding of a block’s own runs, inside the block code, recovers the grouping that cutting the transform into blocks of 63 threw away.

Named alongside this one

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

The objects this essay names

Each one links to every other essay that touches it.

Bidirectional indexCompressed bit vectorEntropyIndex sizeRank querySpace accountingTradeWavelet tree