The index that replaces the text

The same occurrences, less bookkeeping

Two traversals examine identically many phrases and report identically many occurrences. What differs is that one holds every occurrence found so far in a set and the other holds an ordered list and a cursor.

Eight patterns of six characters on a collection of eight near-identical copies — three thousand and seventy-two characters, two hundred and five phrases.

Both traversals examine 1,370 phrases for the first query, and 1,370 for the other. Identical, on every query.

Both report the same occurrences, in the same order, at the same positions.

What differs is what each holds while doing it.

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. 1 Eight queries: the occurrences found, the visited set the published queue holds, and the largest frontier the sweep holds.

The two structures

The queue version holds a first-in-first-out list of occurrences to process and a set of every occurrence found so far. The set exists because a queue can reach a position along two paths, and the set stops it being reported twice.

The sweep version holds one sorted list and a cursor. Occurrences ahead of the cursor are pending; occurrences behind it are done; and a production is inserted at its sorted position, which every copy points right guarantees is ahead.

No set. A position cannot be processed twice, because the list holds no duplicates and the cursor only moves forward.

The measurement

Per query, the visited set holds every occurrence found: 8, 7, 16, 31, 8, 8, 15, 7 across the eight patterns.

The sweep’s largest frontier is 4, 4, 11, 19, 4, 5, 9, 4.

Ratios of 2.00, 1.75, 1.45, 1.63, 2.00, 1.60, 1.67, 1.75.

So the sweep holds between a half and two thirds of what the queue holds, and the factor is a small constant rather than an asymptotic improvement.

The ratios are worth reading as a distribution rather than as an average, because they order themselves by the answer’s size.

The two-occurrence and seven-occurrence queries give 2.00 and 1.75. The sixteen-occurrence query gives 1.45 and the thirty-one-occurrence one gives 1.63.

So the factor is largest on the smallest answers, which is the opposite of what a saving would want. A query with two occurrences holds four positions in a set and two in a frontier — a saving of two positions, on a query that was cheap anyway.

That the factor does not grow with the answer is the whole of a constant factor, not a term, and it is visible here in the individual queries before any sweep.

Why the phrases examined are identical

Both traversals do the same thing at each occurrence: walk the phrases in source order, stop when a source exceeds the current position, and produce a copy for every phrase whose source region contains it.

That inner loop is unchanged. What the sweep changes is only the order in which occurrences are visited, and every occurrence is visited exactly once either way — so the inner loop runs exactly as many times.

The equality is worth checking rather than assuming, because a traversal that changed the work would be a different comparison. It is: 1,370 against 1,370, and the same at every copy count.

That makes this a clean measurement of a bookkeeping change, with the work held fixed by construction rather than by argument.

What the frontier is

The sweep’s frontier is the part of its sorted list ahead of the cursor — the occurrences known and not yet processed.

It starts as the primaries, grows as productions are inserted, and shrinks as the cursor advances. Its maximum over a query is what the plate reports.

On a collection of d near-identical copies the propagation is a chain: an occurrence in copy one produces one in copy two, which produces one in copy three, and so on. In text order that means the cursor is always near the front of the list, and the frontier is the productions that have run ahead of it.

A parse with nested repetition would give a different shape — a fan rather than a chain — and a larger frontier.

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. 2 What each traversal holds during one query, against how repetitive the collection is. Both rise with the copy count.

Both are proportional to the answer

The plate’s important feature is that both lines rise, and rise together.

The visited set is the answer, by definition: every occurrence found is in it.

The frontier is a fraction of the answer, because on a chain propagation the productions run ahead of the cursor by a bounded amount and that amount is proportional to how many copies there are.

So the substitution removes a constant factor rather than a term. A constant factor, not a term is that result, and it is smaller than the deferral naming this work expected.

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: 2.50x at 2 copies and 1.42x at 32, which is a factor that shrinks rather than grows. Against a structure of 17,040 bits whose whole claim is being proportional to the phrase count, the query holds 7.5% more that nothing in its own accounting mentions.05001e+3102030copies of the basebits held during one querythe visited set: 1,274the frontier: 8966-character patterns2.50x to 1.42x
Fig. 3 The same measurement on a corpus built to repeat, where the propagation chains are longer and the frontier correspondingly larger.

There is a shape of collection where the frontier would be much smaller than the answer, and it is worth naming so the constant is understood as a property of these collections rather than of the method.

Suppose the propagation is a long chain with no branching: occurrence one produces occurrence two, which produces three, and so on. In text order the cursor processes one, inserts two ahead, processes two, inserts three. The frontier is never more than one.

The visited set, meanwhile, holds all d of them.

So on a pure chain the factor would be d rather than 1.6 — an asymptotic saving rather than a constant one.

The collections measured here are not pure chains: a pattern in the base occurs in several places within each copy, so each occurrence produces several, and the frontier holds them all. That fan-out is what turns a would-be factor of d into a factor of 1.6.

Which shape a real collection has is not something this strand measures, and it is the question that decides whether the substitution is worth a constant or a term.

What a set costs that a list does not

The factor of 1.6 is the count of positions held, and there is a second difference the count does not capture.

A hash set of positions has a load factor, a hash function and a memory layout; a sorted array of the same positions is contiguous. So the same number of positions costs more in a set than in a list, by whatever a hash table’s overhead is — typically a factor of two in memory and a scattered access pattern.

That is not measured here, because this collection charges bits held rather than bytes allocated. What can be said is that the substitution removes a hash table from a query’s inner loop and replaces it with a binary search into a sorted array, and the two have different constants that this instrument cannot see.

The comparison that is not one comparison is the habit that requires saying so: an operation count is a count of operations and the operations are not all the same size.

The order the answers come out in

Both traversals produce their occurrences in an order and neither produces them sorted, which is worth stating because a caller might rely on it.

The queue produces breadth-first over the copy graph: all the occurrences at depth one, then all at depth two. In text terms that is scattered.

The sweep produces them in text order, because it processes them in text order and reports each as it goes.

So the sweep’s output is sorted and the queue’s is not, and both implementations sort at the end because the interface promises sorted occurrences.

That the sweep produces them sorted for free is a small additional saving nobody asked for: occ log occ comparisons removed, on an answer of thirty, which is nothing. It would be something on an answer of a million.

It is also a property a caller could come to rely on, which is the failure mode one set, three orders is about — a representation chosen for one reason supplying an order that becomes load-bearing. Here the order is a consequence of the traversal rather than of a representation, and it is stable in a way a code shape’s leaf order is not.

Against the structure it sits in

The comparison worth making is between the query’s working memory and the index’s own size, because a structure whose whole claim is being proportional to the phrase count has a query holding something else.

At thirty-two copies over twelve thousand characters: the index is 20,640 bits and the visited set is 420. Two per cent of the structure, held during a query and released after.

At thirty-two copies the answer is thirty occurrences, so the set is thirty positions at fourteen bits. On a collection where a pattern occurs a thousand times it would be fourteen thousand bits, which is most of an index of this size.

So the working memory is proportional to the answer and the structure is proportional to the phrase count, and there is no relationship between the two. A query with many occurrences on a highly compressible collection holds far more than the index it is querying.

That is a genuine gap in how this family accounts for itself, and it is not specific to the visited set: any method reporting occ occurrences holds occ of something. What the sweep changes is the constant.

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. 4 The property the sweep rests on: every copying phrase points left, so every produced occurrence lies to the right of its source.
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 The cost the two traversals share: finding which phrases contain a position, which is where the propagation’s work actually goes.

Putting the shared cost beside the differing one gives the honest proportion.

Per query on this collection, the two traversals each examine about seventeen hundred phrases. That is the propagation’s work, and it is unchanged.

Each also holds between four and thirty-one positions. That is the bookkeeping, and it differs by a factor of 1.6.

So the substitution changes a quantity that is two orders of magnitude smaller than the one it does not change. A reader looking for a speedup should look at the scan; a reader looking for a memory reduction has found one, and it is small.

The scan the order does not touch is where the larger quantity is attacked, and it gets a factor of nineteen — which is the number this strand actually has to offer on cost.

What the equality check is for

The two traversals must report the same occurrences, and the check is on the occurrences rather than on their number.

That distinction matters because the two produce them in different orders — the queue in breadth-first order over the copy graph, the sweep in text order — so a comparison of counts would pass on a traversal that found a different set of the same size.

Measured: seventy-two comparisons across two corpora, three copy counts and three pattern lengths, fourteen hundred and seventy-one occurrences, every one matching.

A difference would not be a slower traversal; it would be a wrong answer. Which is why this is the primary check and the operation counts are secondary.

The same sweep, run from the right, finds two of sixteenWhat the order is worth, measured by taking it away. The sweep visits its known occurrences in text order and inserts each one it produces ahead of the cursor; run from the right instead, an occurrence produced to the right of the cursor lands BEHIND it in that order and is never processed. On a collection of 16 copies the correct traversal reports 16 occurrences and the reversed one reports 2 — it loses 14, which is 87.5%. Every occurrence it does report is genuinely there, so the failure is silent: the answer is short and nothing but a comparison against the queue can see it. This is the phrase index's version of a defect the document listing had one strand ago, on a different object and in the same shape.occurrences reportedin text order16from the right2 — 14 lostevery one it reports is real, so the answer is short rather than wrong1 of them were found by the boundary search and never propagated16 copies · 6-character pattern87.5% lost
Fig. 6 What the ordering is worth, measured by removing it: the same sweep run from the right, reporting two occurrences of sixteen.

What a query holds, in general

The comparison above raises a question this family’s accounting does not answer, and it is worth stating as an open one rather than pretending it is settled.

Every structure in this collection reports a size: bits held at rest, in parts, checked. None of them reports a query working set: bits held during a query and released.

For most of them that is fine, because the working set is a few registers. A backward search holds an interval. A locate holds a row and a step count. A rank walk holds a position.

Two structures in this slate break that: the document listing’s answer-so-far bitmap, which is d bits, and this propagation’s visited set, which is occ positions.

Both are proportional to something about the query rather than about the structure, and neither appears in any size the structure reports. So a system sizing its memory from these essays’ numbers would be sizing for the structures and not for the queries.

A document already in the answer is where the first of the two was found and where the trap in clearing it was recorded. What neither essay does is add a working-set column to the accounting, and doing so would be a change to every structure here.

That is a gap named rather than closed, and the reason to name it in this essay is that this is the one place where the working set is the entire subject.

What would make it a term

Since the substitution is a constant here, it is worth saying what collection would make it more, because that is the question a reader with a different corpus has.

The frontier is the number of occurrences known and unprocessed at once. The visited set is the number found in total. So the ratio is (total found) over (peak pending), which is large when the propagation is deep and narrow and small when it is shallow and wide.

Deep and narrow: a chain of versions, each copying the previous one, where a pattern occurs once per version. Each occurrence produces exactly one, the frontier is one, and the ratio is the version count.

Shallow and wide: a base document copied d times in parallel, where each copy’s phrase points at the base. Every occurrence in the base produces d − 1 at once, the frontier is d − 1, and the ratio is about one.

The collections measured here are the second shape, because copiesOf builds each copy from the previous text as a whole rather than chaining phrase by phrase.

So the 1.6 is a measurement of a parallel-copy collection, and a versioned archive whose revisions each derive from the last would give a larger number. That is a plausible real shape and this strand does not measure it, which is worth admitting rather than generalising past.

What the sweep is not

Two things it might be mistaken for.

It is not a faster propagation. The phrases examined are identical, so the dominant cost — the scan over phrase sources — is unchanged. The scan the order does not touch is where that cost is attacked, by a different change.

It is not a smaller index. The visited set is query state and the index reports the same size either way.

What it is: a traversal that needs less state, by an amount that is a constant factor, resting on a property of the parse that has to be checked because a parse without it loses occurrences silently.

Where the primaries come from

The propagation starts with the primary occurrences and it is worth saying where those come from, because they are the sweep’s initial list and their number decides how much of the answer is not produced at all.

A primary occurrence crosses a phrase boundary. Finding them means splitting the pattern at each of its m − 1 split points and asking which boundaries have the left half ending at them and the right half starting at them — an intersection of two ranges, which the occurrences that cross a boundary measures.

On the collections here the primaries are one or two occurrences and the secondaries are the rest: at thirty-two copies, one primary and twenty-nine secondaries.

That ratio is the whole reason the propagation matters. A method finding one occurrence by search and twenty-nine by arithmetic is a method whose cost is dominated by the arithmetic, and the arithmetic is what both traversals perform.

It also explains why the sweep’s initial list is short. The frontier starts at one or two and grows only through production, so its maximum is entirely a property of the propagation’s shape rather than of the search.

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. 7 Where the index’s own bits are, for contrast with the query’s: nearly half of a phrase index is three permutations of its boundaries.

Why it is worth doing anyway

A constant factor of 1.6 on query working memory is a small result and there are two reasons to take the substitution.

It removes a structure. A hash set in an inner loop is a thing that can be got wrong, that allocates, and that has a load factor. A sorted list and a cursor have none of those.

And it makes a property explicit. The sweep’s guard states, at the point of every production, that a copy lies to the right — which is a fact about the parse that was previously implicit and unchecked. A parse that lost the property would now fail loudly rather than producing short answers.

That second is worth more than the memory. This collection has found the same class of defect twice in two strands — an ordering that a traversal depends on and nothing enforces — and the guard is what turns the dependence into something a gate can see.

The third reason is smaller and worth having: the sweep is easier to reason about. A queue with a visited set requires a reader to hold two invariants — that everything in the queue is unprocessed and that everything in the set has been produced — and to check that the two stay consistent. A cursor into a sorted list requires one: everything before the cursor is done.

That is not a measurement and it is the kind of thing this collection is usually suspicious of, since “simpler” is a judgement rather than a number. What makes it defensible here is that the simplification comes with a checkable claim attached — the ordering property — so the reduction in invariants is not a matter of taste but a consequence of a fact about the parse that the code now asserts.

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,02492 phrases · 1,024 characters60 drawn, every one pointing right
Fig. 8 The fact the whole substitution rests on, on a four-symbol alphabet where the phrases are shorter and more numerous.

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.

Lempel ziv parsePhrase indexPropagationSecondary occurrenceTraversal orderVisited setWorking memory