workshop private

← all creations

Narrow

viz · created 2026-09-28

Arithmetic coding drawn as the one thing it actually is — an interval that keeps a slice of itself per symbol until its width is the message's probability, and a code length that is minus the log of that width. On a 99/1 source 400 symbols come out in 47 bits while Huffman, the provably optimal prefix code, spends 400, because a whole bit is the smallest thing it can spend and this symbol is worth 0.014 of one. Then the model is set wrong and arithmetic coding loses to Huffman by two bits.

algorithmscanvas

A prefix code has to hand out a whole number of bits per symbol. That is the entire limitation and it is not a small one. A symbol that arrives 90% of the time carries −log₂(0.9) = 0.152 bits of information, and Huffman — which is provably the best prefix code that exists — still has to spend a whole bit on it, because half a bit is not a thing you can write down. Over a long message that rounding is not noise. It is a fixed tax on every symbol, and on a skewed source the tax is several times larger than the payload.

Arithmetic coding gets out of it by refusing to encode symbols at all. It encodes the whole message as a single number, and it finds that number by narrowing an interval:

Start with [0, 1). Subdivide it in proportion to the model. Keep the slice belonging to the symbol that actually arrived. Subdivide that slice by the same proportions. Keep the next symbol’s slice. Repeat.

After n symbols the surviving interval has width

w = p(s₁) · p(s₂) · … · p(sₙ) = P(message)

— that multiplication is the whole algorithm — and any number inside it names the message uniquely. Writing down a number inside an interval of width w costs about −log₂ w bits. So the code length is the message’s own information content, to within two bits total, not per symbol. The fractional bits are never rounded away. They accumulate inside the interval and get spent once enough of them have piled up.

That is what the piece is built to draw: the width of a bar and a number of bits are the same quantity on two scales.

What’s on screen

The four numbers, at 400 symbols

Every figure below is read off the running demo by scripts/screenshot-demo.mjs, which checks 47 claims on every build.

source−log₂ParithmeticHuffman
fair coin, 50/50400.0402400Huffman wins
skewed, 90/10213.02144001.87×
almost always A, 99/145.6474008.51×
four symbols, 40/30/20/10759.07607831.03×

Three things fall out of that table.

The overhead is a constant, not a rate. Arithmetic never spends more than 2.00 bits over −log₂P — checked across all twelve source/model combinations, and the worst case is the fair coin, where the flush at the end is the entire cost. Huffman’s overhead is up to a bit per symbol, which is why its column never moves off 400 no matter how lopsided the source gets.

On a fair coin, arithmetic coding loses. 402 against 400. There is nothing fractional to recover when every symbol is worth exactly one bit, so the only thing left is the two-bit flush. Worth stating plainly, because the usual telling of this algorithm does not have a case where it comes second.

Huffman is not being sandbagged. Its tree is built from the actual symbol frequencies of the message — the optimal two-pass static prefix code, the best one that exists for this exact input — and it is not charged for transmitting the table it would need. The gap is not a bad tree. It is the whole-bit constraint, which no prefix code escapes.

The bits that are deferred

The register is finite, so the interval has to be renormalised as it shrinks. Two of the three rules are obvious: if the interval sits entirely below ½ the answer starts with 0, and if it sits entirely above ½ it starts with 1 — emit the bit and double what is left.

The third rule is the one that makes the algorithm work. An interval can straddle ½ while being narrower than the register can hold, and then the next bit is genuinely not decided yet while the precision is about to run out. The fix is to notice that whatever that bit turns out to be, the one after it is its opposite. So the coder counts a pending bit, expands around the midpoint, and carries on; when a real bit finally lands, the deferred ones follow it inverted.

Those are the hollow cells on the tape: bits whose count is known and whose value is not. Almost no explanation of arithmetic coding draws them, and they are the reason a fixed-width register can code an interval far narrower than itself. Set the register to 6 bits and the same 400-symbol message still comes out in 213 bits and still decodes exactly — a register that can hold 64 distinct values, addressing an interval 2⁻²¹³ wide.

Push it further and the point gets blunt. At 4000 symbols the interval width is 2⁻¹⁹²⁰, which underflows a double to exactly zero — the demo’s own realWidth readout goes to 0 and stays there — while the 16-bit coder produces 1922 bits and round-trips perfectly. The number being encoded is not a number any float can hold. The coder never holds it.

The model is the whole game

Switch the model to uniform on the 90/10 source and arithmetic coding spends 402 bits against Huffman’s 400. It loses. The bit tape gives away why: with a uniform model over two symbols each one costs exactly one bit, the low half is 0 and the high half is 1, and the “code” is just the message spelled out. Compression is not something the coder does. It is something the model knows, and the coder’s only job is to spend the model’s beliefs without rounding them.

Which makes the third setting the interesting one. Adaptive starts from one imaginary observation of each symbol — it has been told nothing about the source — and updates its counts as it codes. On 90/10 it lands on 217 bits, three bits behind the model that was handed the right answer, and 183 under Huffman. On 99/1 it reaches 52 against 400. On the four-symbol source it comes in at 768 against Huffman’s 783, and unlike Huffman it sends no table at all, because the decoder rebuilds the same counts from the symbols it has already decoded.

That is the practical form of the whole piece: the coder is within two bits of whatever its model believes, so all the engineering that is left is believing better things.

Notes