Learn · The Art of Not Looking
budding
Packing the Suitcase
Sortedness has already paid for most of the next number; encode only the surprise that remains.
“We frequently represent sets of document or row identifiers by arrays of integers.”
— Daniel Lemire, Nathan Kurz, and Christoph Rupp, “Stream VByte: Faster Byte-Oriented Integer Compression”
This is the twelfth chapter in a book about search from first principles. The previous chapters avoided scoring and decoding regions that could not win. Here you will remove bits that order and repetition have made predictable. You will derive the information floor for a sorted subset, transform postings into gaps, build Elias gamma and delta and Golomb–Rice codes, compare them with variable-byte and block bit packing, front-code a term dictionary, and state the correct canonical round-trip law. You will measure encoded bytes, decoded integers, branches, and cache traffic separately. The next chapter shows that a compressed set can answer rank and select without first becoming a flat list.
Order is free information
Consider document identities
12, 19, 20, 37, 41
Five ordinary 32-bit integers occupy 160 bits. But the list is strictly increasing. Store the first identity and the positive gaps:
12, 7, 1, 17, 4
The values are smaller because each predecessor predicts a lower bound for its successor. The transformation is lossless:
Do not encode information the invariant has already supplied.
The floor beneath every lossless representation
How many increasing lists of distinct identities can be chosen from a universe of identities? Exactly
Any uniquely decodable fixed-length representation of all such sets therefore needs at least
bits in the worst case. This is a counting argument: fewer bit strings cannot name every possible set.
For sparse sets, Stirling's approximation gives the useful scale
The first term resembles storing an average gap of ; the second records that gaps vary. A measured codec can be reported as bits per posting and as excess over this corpus-independent combinatorial floor.
The floor does not predict decode speed, random access, update cost, or block metadata. It tells us only how far a representation is from the minimum number of distinguishable states.
Prefix-free code — no complete codeword begins another, so a concatenated stream can be decoded without separators. Learn more.
Elias gamma: write the magnitude twice
For a positive integer , let
Its ordinary binary representation has bits. Elias gamma writes zeroes followed by that binary representation. Examples:
| binary | gamma | length | |
|---|---|---|---|
| 1 | 1 | 1 | 1 |
| 2 | 10 | 010 | 3 |
| 3 | 11 | 011 | 3 |
| 4 | 100 | 00100 | 5 |
| 7 | 111 | 00111 | 5 |
Hence
The unary prefix tells the decoder how many remaining binary bits to read. The code is simple and universal—it needs no fitted source parameter—but it performs bit-level control work for every integer.
Elias delta: compress the magnitude
Gamma writes the magnitude in unary. Delta instead gamma-codes the binary length , then appends the low bits of :
Delta eventually improves on gamma for large integers, while adding another dependent decode step. “Universal” means the expected length remains within a bounded-factor relationship for broad source families; it does not mean the code wins every finite workload.
Golomb–Rice: fit the gaps you actually have
Assume positive gaps follow a geometric distribution
Choose modulus . Write , encode quotient in unary, and encode remainder in a truncated binary code. Rice coding restricts , so the remainder always uses exactly bits.
Let . For Rice modulus ,
Therefore the expected code length is
Balancing unary quotient and remainder suggests an unrestricted Golomb modulus near
with Rice choosing a nearby power of two. Estimate from the actual gap distribution and include the parameter in the format. A fitted code can beat a universal one, but the fit can age as the corpus changes.
Variable byte: spend bits to simplify control
Variable-byte coding groups payload into seven-bit chunks and uses one control bit per byte to mark continuation or termination. A nonnegative integer uses
bytes.
It wastes up to seven payload positions at a byte boundary and repeats control bits. In return it is simple, byte-aligned, and easy to integrate. Its scalar decoder branches on each control bit; predictable length distributions help the branch predictor, irregular ones do not.
Stream VByte separates control bytes from payload bytes. Four two-bit length codes describe four integers, letting a decoder load and shuffle payload with SIMD instructions instead of discovering each length through a dependent branch. The paper's reported throughput is tied to its Haswell machine, compiler, data movement boundary, and corpus. It is evidence for a design, not a timeless speed constant.
Bit packing: one width per block
For a block of gaps, compute
and store all gaps in exactly bits plus the block header. The payload costs bits. Vectorized unpackers process fixed groups with shifts, masks, and SIMD lanes.
One outlier can set for the whole block. Patched schemes store most values at a smaller base width and record exceptions separately. This improves size when exceptions are rare but adds streams, branches, and reconstruction work.
The block size joins two tradeoffs already seen in Chapter 11:
- small blocks adapt widths closely and add more headers;
- large blocks amortize headers and expose more outliers;
- fixed-size SIMD blocks simplify loops but may not align with skip blocks or cache lines.
The `perf-search-layout` discipline adds a machine question: after decoding, are the integers consumed sequentially, searched in place, or materialized into another array? A codec that expands into a cache-cold buffer can lose to a larger representation queried directly.
Front coding the dictionary
Sorted terms share prefixes:
compress
compressed
compression
compressor
Store the first term, then each common-prefix length and remaining suffix. In blocks, keep one full restart term followed by front-coded entries. Restart spacing trades random lookup work against saved bytes.
Front coding removes repeated bytes only because lexicographic order clusters prefixes. It does not replace the dictionary lookup structure from Chapter 2. A term table may keep restart offsets or a separate search layout so lookup does not decode from the beginning.
The round-trip law belongs to the wire form
Suppose an encoder sorts and deduplicates postings before writing them. Then an arbitrary in-memory sequence need not satisfy
The format never promised to preserve order or duplicates. Its canonical law is
Also pin intermediate bytes for known examples. A round-trip test alone can be satisfied by two mutually inverse but wrong functions. The encoded bytes prove which format was chosen.
Canonical gap encoding
ENCODE-POSTINGS(IDENTITIES, CODEC)
Input: finite document IDENTITIES, declared positive-integer CODEC
Output: canonical encoded bytes
ordered ← SORT-UNIQUE(IDENTITIES)
previous ← 0
output ← empty byte sequence
for each identity d in ordered
gap ← d - previous
output ← APPEND(output, ENCODE-POSITIVE(CODEC, gap))
previous ← d
return outputBounds precede decoding. A decoder validates block counts, bit widths, payload lengths, checked products, and cumulative document identities before allocating or publishing output. Truncation, overflow, nonpositive gaps, and identities outside the corpus are typed failures, not partial lists.
Measure the whole representation path
For each corpus stratum and codec, record:
- encoded bytes and bits per posting;
- excess over ;
- encode and decode throughput;
- single-list latency and batched throughput;
- branches, branch misses, instructions, and cache misses where available;
- peak decoded scratch and retained memory;
- seek or restart cost for partial access;
- scalar and SIMD paths on every supported architecture;
- end-to-end query latency, not decoder microbenchmarks alone.
The static-layout lesson matters here: headline SIMD throughput often assumes several independent blocks in flight and data moving between named cache levels. Single-query latency can improve less. Publish the boundary that was measured.
No codec wins the entire plane. Gamma and delta are compact bit codes with dependent decoding. Golomb–Rice fits geometric gaps. Variable byte favors simplicity. Stream VByte separates controls for SIMD. Fixed-width block packing spends headers to make wide parallel decoding possible. A small flat array can beat all of them when the list is tiny.
Lessons
- Sorted postings imply positive gaps; encode the gaps rather than repeated absolute identities.
- A sorted -subset of a -element universe needs at least bits to distinguish every possibility.
- Elias codes need no fitted source parameter; Golomb–Rice spends a parameter to match geometric gaps.
- Byte alignment and separated controls may use more bits and decode faster.
- Bit packing trades block headers and outlier sensitivity for fixed-width SIMD work.
- Front coding exploits prefix sharing but needs restart points for bounded access.
- Canonicalizing encoders obey encode–decode–encode, not necessarily decode–encode identity on arbitrary inputs.
- Round trips must pin known encoded bytes.
- Measure transport size, decode work, retained memory, and query consumption as separate boundaries.
Practice
- Gap-encode , gamma-code the gaps, and count the bits.
- Compute and compare it with four fixed four-bit identities.
- Derive using the tail-sum formula for a nonnegative integer random variable.
- For gaps , compare one four-value bit-packed block with two two-value blocks, including one width header per block.
- Give a sequence for which decode–encode does not preserve the in-memory value but encode–decode–encode preserves canonical bytes.
- Design a benchmark whose decoder-only winner loses end-to-end because it materializes more scratch or prevents direct searching.
References
- Peter Elias. “Universal Codeword Sets and Representations of the Integers.” IEEE Transactions on Information Theory 21, no. 2, 1975.
- Solomon W. Golomb. “Run-Length Encodings.” IEEE Transactions on Information Theory 12, no. 3, 1966.
- Daniel Lemire and Leonid Boytsov. “Decoding Billions of Integers per Second through Vectorization.” Software: Practice and Experience 45, no. 1, 2015.
- Daniel Lemire, Nathan Kurz, and Christoph Rupp. “Stream VByte: Faster Byte-Oriented Integer Compression.” Information Processing Letters 130, 2018.