The other axis

Half an index is three permutations

A phrase index stores a length, a source and a literal per phrase — and three orderings of its boundaries, at forty-five per cent of the structure. One of the three is the inverse of another, and nothing needs both at once.

A Lempel–Ziv index over four thousand characters with two hundred and twenty-six phrases: 11,978 bits.

The parse — a length, a source and a literal per phrase — is 6,554 of them.

The boundary orders are 5,424. Forty-five per cent of the structure.

Almost half the phrase index is three permutations of its boundariesWhere a 226-phrase index over 4,096 characters keeps its bits. The parse itself — a length, a source and a literal per phrase — is 6,554. The boundary orders are 5,424, or 45.3% of the whole structure, and they are three permutations of 226 elements at 1,808 bits each: the boundaries in suffix order, the boundaries in reverse-prefix order, and the INVERSE of the second. Nothing in the index needs the second and third at once — the search walks a range in one and tests membership in the other — so the third is a time-for-space choice worth 15.1% of the structure that has never been priced here. The published alternative is a shortcut representation answering both directions from one array.phrase lengths2,71222.6%phrase sources2,71222.6%phrase literals1,1309.4%boundary orders5,42445.3%three permutations of 226 elements: 1,808 bits each, and one is the inverse of another226 phrases · 4,096 characters45.3% in the orders
Fig. 1 Where a phrase index keeps its bits: four parts, of which the largest is three permutations of the phrase boundaries.

What the three orders are

A primary occurrence crosses a phrase boundary, and finding one means splitting the pattern and asking two questions about each split point.

Which boundaries have text starting with the right half? That needs the boundaries sorted by the suffix beginning at them — bySuffix.

Which boundaries have text ending with the left half? That needs them sorted by the reversed prefix ending at them — byPrefix.

And is a given boundary inside the second range? That needs the position of a phrase in byPrefix, which is its inverse — rankInPrefix.

Three arrays of z entries at ⌈log₂ z⌉ bits each: 1,808 bits apiece.

One thing the three orders are not is a suffix array. A suffix array over the text has n entries; these have z, one per phrase boundary, and that difference is the whole reason the family is small. The orders are a suffix-array-like structure restricted to the boundaries, which on a repetitive collection is a hundredth of the positions.

So a reader who sees “three orderings of z elements” and thinks it sounds like a lot should compare against the alternative: an index over the same text with a full suffix array is n⌈log₂ n⌉ = 49,152 bits on this collection, against the whole phrase index’s 11,978.

Forty-five per cent of a structure that is a quarter of the obvious one is not a large number. It is a large share, which is a different thing, and the reason it is worth an essay is that shares are where the next saving is.

The one that is an inverse

rankInPrefix is the inverse permutation of byPrefix. Given one, the other is a linear-time computation and a z-entry array.

Both are stored, and nothing in the index needs both at once.

The search walks a range of byPrefix — or of bySuffix, whichever is smaller — and tests membership in the other. Those are two different operations on two different arrays, and the membership test is what rankInPrefix exists for.

So the index stores a permutation and its inverse because it does two things with one ordering: iterate it and index into it. That is a completely ordinary reason and it is a time-for-space choice that has never been priced here.

1,808 bits — fifteen per cent of the whole index.

What the alternative is

Storing a permutation and answering both directions from one array is a solved problem with a published shape: keep the permutation and a sampling of shortcut pointers, so that π⁻¹ is answered by walking a cycle to the nearest shortcut.

At a sampling of one in t, the structure is z log z + (z/t) log z bits and π⁻¹ costs t steps. At t = 4 that is a quarter of an extra array rather than a whole one, and four steps per membership test.

So the choice is: 1,808 bits, or 452 bits and four steps per test.

Whether that is worth taking depends on how many membership tests a search performs, which is the points this collection’s phrase index already counts — it is the number of candidates the intersection examines, and it is reported rather than assumed away.

That number is available and the trade is not measured. This essay prices the option; it does not take it.

Almost half the phrase index is three permutations of its boundariesWhere a 146-phrase index over 4,096 characters keeps its bits. The parse itself — a length, a source and a literal per phrase — is 4,234. The boundary orders are 3,504, or 45.3% of the whole structure, and they are three permutations of 146 elements at 1,168 bits each: the boundaries in suffix order, the boundaries in reverse-prefix order, and the INVERSE of the second. Nothing in the index needs the second and third at once — the search walks a range in one and tests membership in the other — so the third is a time-for-space choice worth 15.1% of the structure that has never been priced here. The published alternative is a shortcut representation answering both directions from one array.phrase lengths1,75222.6%phrase sources1,75222.6%phrase literals7309.4%boundary orders3,50445.3%three permutations of 146 elements: 1,168 bits each, and one is the inverse of another146 phrases · 4,096 characters45.3% in the orders
Fig. 2 The same accounting on a corpus built to repeat, where the parse has fewer phrases and each part scales with it.

There is a cheaper alternative to a shortcut structure that is worth mentioning because it costs nothing and does not always apply: iterate the other array instead.

The search filters the smaller of the two ranges against the other’s rank window. If it always filtered byPrefix — walking that range and testing membership in bySuffix — then the inverse it needs would be of bySuffix rather than of byPrefix, and the index would still store two orders and one inverse.

What it cannot do is avoid an inverse altogether, because the two operations are iterate-one and index-into-the-other, and whichever way round they are assigned, one array is being indexed into by phrase identity.

Unless the filtering is always done on the same side — which loses the “smaller range” optimisation and costs whatever that is worth. The index counts the points examined, so that trade is measurable and is not measured here.

Why the orders are so large

Forty-five per cent is more than a reader would guess and the arithmetic explains it.

The parse costs z(2⌈log₂ n⌉ + ⌈log₂ σ⌉) — two positions and a literal per phrase. At n = 4,096 and σ = 22 that is z(12 + 12 + 5) = 29z.

The orders cost 3z⌈log₂ z⌉. At z = 226 that is 3z·8 = 24z.

So the two are within twenty per cent of each other, and the reason is that ⌈log₂ z⌉ is not much smaller than ⌈log₂ n⌉ — eight against twelve, on a collection where the parse has a phrase every eighteen characters.

On a more repetitive collection the gap widens in the orders’ favour. A collection where z is a hundredth of n has ⌈log₂ z⌉ four bits below ⌈log₂ n⌉, so the orders are relatively cheaper. On a less repetitive one they approach the parse.

Every copy points left, and that is the whole of the substitutionThe first 60 copying phrases of a 1,024-character collection, each drawn as a line from the text it copies to the text it produces. Every line points right, and it must: a greedy self-referential parse chooses a phrase's source from text already produced, so a source is always earlier than the phrase that uses it, by construction rather than by luck. That single fact is what removes the visited set from secondary propagation — an occurrence produced by a phrase lies strictly to the right of the occurrence it was copied from, so a sweep in text order reaches each one exactly once and cannot produce one twice. The parallel to the document listing's chain is exact: there it was left-first and here it is left-to-right, and both times the array being deleted recorded what the traversal order already knew.01,024120 phrases · 1,024 characters60 drawn, every one pointing right
Fig. 3 The boundaries the three orders are orderings of: each phrase drawn as a line from the text it copies to the text it produces.

What a permutation costs to store

Since three of the four parts are permutations, it is worth being precise about what one costs and what it could cost.

A permutation of z elements has z! arrangements, so it carries log₂(z!) bits of information. By Stirling that is z log₂ z − 1.44z, which at z = 226 is 1,489 bits.

Stored as an array of indices it is z⌈log₂ z⌉ = 1,808. So the plain representation is 21% above the information content — which is small, and it is entirely the ceiling function plus Stirling’s linear term.

That gap is the whole of what any encoding of a permutation can win, and this collection has measured it once already in a different strand: sixteen per cent is the number there, on a different z, and the shape is the same.

So the three orders cannot be coded below about 4,470 bits by any means. What can be done is not storing the third, which is a structural change rather than a coding one and is worth 1,808 against the 954 a coding could win on all three.

The structural saving is larger than the coding saving, which is a reliable pattern when a structure holds something derivable: deriving beats compressing.

What the deferral had expected

The sentence naming this strand’s work called the phrase index’s chain the largest part of a different apparatus.

It is not. The largest part is the boundary orders, and the thing the deferral called the chain — the propagation’s visited set — is query state that no accounting mentions at all.

A constant factor, not a term is that retraction, and this essay is where the actual largest part is identified. The two are a pair: a strand looking for a size result in one place found it in another, by measuring the structure in order to price something else.

That the measurement happened at all is incidental. The running maximum of the scan the order does not touch costs 2,821 bits and reporting it as a share required knowing the index’s parts, and knowing the parts is what showed that half of it is permutations.

The same occurrences, and the two things one traversal has to remember8 patterns of 6 characters on a collection of 8 near-copies, 3,072 characters, 205 phrases. Each row is one query: the occurrences found, the visited set the published queue holds, and the largest frontier the sweep holds. The two traversals examine exactly the same 17,715 phrases and report exactly the same occurrences — that is the check, and a difference would be a wrong answer rather than a slower one. What differs is the bookkeeping: the queue keeps every occurrence found so far in a set, and the sweep keeps one ordered list and a cursor."t than"8 found · set 8 · frontier 4"of her"7 found · set 7 · frontier 4" that "16 found · set 16 · frontier 11" every"31 found · set 31 · frontier 19"the ev"8 found · set 8 · frontier 4"o of c"8 found · set 8 · frontier 5" than "15 found · set 15 · frontier 9"ime ra"7 found · set 7 · frontier 4the visited set, pale; the sweep's frontier, dark17,715 phrases examined either way1.67x on what is held
Fig. 4 What the index’s bits are spent to support: eight queries, their occurrences, and the bookkeeping two traversals hold.

The other size question

There is a second thing the parts make visible and it is worth a paragraph because it is the family’s real size problem.

The parse stores each phrase’s source as an absolute position: ⌈log₂ n⌉ bits, 2,712 of the index’s 11,978.

A source is the position of an earlier occurrence, and on a collection of copies it is almost always exactly one copy-length behind the phrase. So the displacements — at − source — are highly repetitive, and storing them instead of absolute positions would compress well.

Nothing here does that, and it is the standard first optimisation in this family’s implementations. Its size is 23% of the index, so getting most of it would be worth more than the inverse permutation.

That is named rather than built, and it belongs in the same list as the shortcut permutation: two size options, both standard, both unpriced here, together worth perhaps a third of the structure.

The other half of the propagation, which the order does not touchFinding which phrases' source regions contain a position, two ways, over 40 positions of a 6,144-character collection with 238 phrases. The index does it by walking every phrase whose source is at or before the position: 207.0 phrases examined a probe. Sorting the intervals by source and keeping a running maximum of their right ends lets a leftward walk stop for good the first time the maximum falls short: 10.9, a factor of 19x, for 21.6% more bits. The two agree at every position. This is deliberately not on the same plate as the sweep: they are two independent changes to one method, and reporting them together would report one saving as two.phrases examined per probethe linear scan207.0sorted, with a running maximum10.9 — 19xthe running maximum costs 2,821 bits — 21.6% of the index40 of 40 positions checked, and the two agree at all of them238 phrases · 6,144 characters19x on the scan
Fig. 5 What one of the three orders is used for, and the structure that made a different lookup nineteen times cheaper for twenty-two per cent more bits.
A constant factor, and not the term the deferral expectedWhat each traversal holds during one query, against how repetitive the collection is. Both lines rise with the copy count, because both are proportional to the ANSWER — and the answer is the copy count, since a phrase of the base occurs once in every copy. So the substitution removes a hash set and a constant factor rather than a term: 20x at 2 copies and 1.58x at 32, which is a factor that shrinks rather than grows. Against a structure of 20,640 bits whose whole claim is being proportional to the phrase count, the query holds 2.0% more that nothing in its own accounting mentions.0100200300400102030copies of the basebits held during one querythe visited set: 420the frontier: 2666-character patterns20x to 1.58x
Fig. 6 A quantity that grows with the collection and is not in this accounting at all: what a query holds while it runs.

The parts plate also makes visible something the strand’s other essays could only assert: that the index’s size does not include the query’s working memory.

At thirty-two copies the index is 20,640 bits and a query holds 420 in its visited set — two per cent, and proportional to the answer rather than to the phrase count.

So the four bars on the plate are a complete accounting of what the structure holds at rest, and an incomplete accounting of what a system running it needs. A constant factor, not a term is where that gap is named, and this plate is where it is visible: there is no fifth bar, and the thing a fifth bar would show is a function rather than a number.

Where the strand’s three results sit together

The strand has three numbers and this essay has the largest, which is worth setting out because the ordering is instructive.

The visited set: a factor of 1.6 on query working memory, which is the deferral’s own subject and the smallest of the three. A constant factor, not a term.

The scan: a factor of 19 on phrases examined per probe, for 22% more bits. The scan the order does not touch.

The orders: 45.3% of the index, with 15.1% of it derivable. This essay, and it is a diagnosis rather than a change.

Two of the three were found by building the first, and the third was found by measuring the index in order to state the second’s cost as a share.

That chain — build a thing, price it, need a denominator, look at the parts, find something larger than the thing built — is a common enough shape in this collection to be worth naming. The denominators are where the results are, and a strand that quotes shares has to compute them.

The permutation strand, next door

This collection has measured what can be won on a permutation once already, and the result bounds what a shortcut structure can do.

The compressed grid’s strand established that sixteen per cent is the whole of what any coding can win on a permutation of z elements, because a permutation of z elements carries log₂(z!) bits and z⌈log₂ z⌉ is only that much above it.

So the three orders cannot be compressed below about 4,556 bits by any encoding. What a shortcut structure does is different — it removes one of the three rather than coding them better — and the two are complementary.

Taking both: 4,556 bits for two coded permutations, plus a sampled inverse at a quarter of one, is about 5,000 against the current 5,424. Which is a disappointing total, and it is the honest one: the orders are near their information content already, and the only real saving is not storing the third.

How the parts move with the collection

The plate is one collection and the parts scale differently, so it is worth saying which way each goes.

The parse is z(2⌈log₂ n⌉ + ⌈log₂ σ⌉). It grows with z linearly and with n logarithmically.

The orders are 3z⌈log₂ z⌉. They grow with z slightly faster than linearly.

So as a collection becomes more repetitive — z falling at fixed n — both shrink, and the orders shrink faster because ⌈log₂ z⌉ falls too. On a highly repetitive collection the orders’ share falls toward a third.

As a collection becomes larger at fixed repetitiveness — n and z growing together — the parse’s ⌈log₂ n⌉ and the orders’ ⌈log₂ z⌉ both grow, and they grow at the same rate, so the shares hold.

So 45% is roughly stable across sizes and falls on more repetitive collections. That means the inverse permutation’s 15% is a share a reader can carry, and it is on the high side for the collections this family is built for.

The other half of the propagation, which the order does not touchFinding which phrases' source regions contain a position, two ways, over 40 positions of a 6,144-character collection with 177 phrases. The index does it by walking every phrase whose source is at or before the position: 149.2 phrases examined a probe. Sorting the intervals by source and keeping a running maximum of their right ends lets a leftward walk stop for good the first time the maximum falls short: 10.9, a factor of 14x, for 21.0% more bits. The two agree at every position. This is deliberately not on the same plate as the sweep: they are two independent changes to one method, and reporting them together would report one saving as two.phrases examined per probethe linear scan149.2sorted, with a running maximum10.9 — 14xthe running maximum costs 2,041 bits — 21.0% of the index40 of 40 positions checked, and the two agree at all of them177 phrases · 6,144 characters14x on the scan
Fig. 7 A structure this accounting had to price, which is what made the accounting happen: a running maximum costing twenty-two per cent of the index.

What the plate is for

A parts plate is the least interesting kind of figure and it is the one that produced the strand’s largest number, which is worth noting as a habit rather than as a result.

Nothing about this measurement is clever. It is the index’s own bits() method, drawn as four bars. It required no new structure, no sweep, no control, and no comparison.

What it required was asking. The index has reported its parts since it was built and nothing had drawn them, because a size in parts is what a structure returns when something asks for a share — and this strand had a share to compute.

An index larger than what it indexes is this collection’s first essay on a size that was not what its account said, and the instrument then was the same: add the parts up and look.

Why the boundaries need orderings at all

Stepping back to what the orders are for, because a reader coming to this family fresh might reasonably ask why a parse needs three sorted arrays.

The index’s whole idea is that a pattern’s occurrences split in two. The ones lying inside a phrase are copies and are produced by arithmetic. The ones crossing a boundary have to be searched for, and there are z boundaries rather than n positions — which is the size saving the family exists for.

Searching z boundaries for “does the pattern’s right half start here” means having them sorted by what follows them. Searching for “does its left half end here” means having them sorted by what precedes. Two questions, two orders.

The third array is the machinery for combining the two answers, which is an intersection: a boundary is a primary occurrence’s location if it appears in both ranges.

So the three arrays are the price of the family’s central idea, and the parse alone would be a compressor rather than an index. The occurrences that cross a boundary is where that split was established here.

That framing also says what the orders’ 45% is buying: it is the difference between a structure that can decompress and one that can search. Compared against that, forty-five per cent is cheap.

Every copy points left, and that is the whole of the substitutionThe first 60 copying phrases of a 1,024-character collection, each drawn as a line from the text it copies to the text it produces. Every line points right, and it must: a greedy self-referential parse chooses a phrase's source from text already produced, so a source is always earlier than the phrase that uses it, by construction rather than by luck. That single fact is what removes the visited set from secondary propagation — an occurrence produced by a phrase lies strictly to the right of the occurrence it was copied from, so a sweep in text order reaches each one exactly once and cannot produce one twice. The parallel to the document listing's chain is exact: there it was left-first and here it is left-to-right, and both times the array being deleted recorded what the traversal order already knew.01,02487 phrases · 1,024 characters60 drawn, every one pointing right
Fig. 8 The parse the orders are orderings of, on a corpus built to repeat: each phrase drawn from the text it copies to the text it produces.

What to carry

Three numbers about a phrase index.

The boundary orders are 45.3% of it, at three permutations of z elements each.

One of the three is the inverse of another, worth 15.1% of the structure, replaceable by a sampled shortcut structure at a quarter of the cost and four steps per membership test.

And the phrase sources are 22.6%, stored as absolute positions where their displacements would compress.

Together those are 38% of a structure whose selling point is being proportional to z, available to two standard techniques, and unpriced in this collection until a strand about something else needed a denominator.

Why an accounting is worth keeping current

The general lesson is not about phrase indexes and it is short.

A structure that reports its size in parts can be asked where its bits are, at any time, for nothing. A structure that reports a total cannot, and the difference shows up years later when somebody wants a share.

Every structure in this collection reports parts, and the convention costs a few lines per bits() method. What it bought here is that a strand about a traversal could, in one call, discover that half the index it was working on is permutations — which is a fact nobody was looking for and which is larger than anything the strand set out to find.

An index larger than what it indexes is where the convention started, on a structure whose total was surprising. The parts convention came out of it and this is the third or fourth time the parts have said something the total could not.

The instruction: report a size in parts, name every part, and never fold two parts together because they are conceptually related. The boundary orders are one part here because they are three arrays serving one purpose, and that grouping is exactly what made the fifteen per cent inside it invisible until somebody divided by three.

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.

Boundary orderIndex sizeInverse permutationLempel ziv parsePermutationPhrase indexSpace accounting