When the algorithm flips a coin

A match decided by a number

Rabin–Karp replaces a comparison of two strings with a comparison of two integers, and pays for it only when the integers agree by accident. On 65,521 windows at a modulus of 1,009 it verified 79 times against a predicted 64.9. On a text of 1,024 characters chosen with the modulus in hand it verified at 64 windows out of 64 possible, each one costing fifteen of the pattern's sixteen characters — and the closed form is silent about that, because it assumes nobody chose the text.

The three algorithms before this one all compare characters. Rabin–Karp does something else: it summarises each window of the text as a number, compares that number against the pattern’s number, and only looks at characters when the two agree.

The summary is a hash, and hashing a window would be pointless if it cost a pass over the window — that would be the naive scan with extra arithmetic. What makes the algorithm work is that the hash rolls: the value for the window starting at i+1i+1 is computed from the value at ii in constant time, by subtracting the character leaving and adding the character arriving.

So the whole text is summarised in one pass of arithmetic, and the character comparisons happen only where two summaries collide.

Verifications against the modulus, measured and predictedA hash collision costs Rabin–Karp a full character-by-character check, and the expected number of them is n/q for a modulus q — the dashed line. The measured counts sit on it across three orders of magnitude, and one does not: at q = 257, one more than the base, the rolling hash becomes an alternating sum and verifies 2.7 times more often than predicted. The formula assumes the hash spreads and says nothing about a modulus chosen without reference to the base — nor about a text chosen with reference to both, which costs 15 of the pattern's 16 characters at every one of 6% of its windows.1,00010,000110100modulus qverifications in one searchq = 257: 693measuredn / qchosen text: 6%one unit = one character comparison · base 2568 colliding blocks at q = 1,009, none at q = 1,000,003
Fig. 1 The cost, measured against the formula that predicts it. A collision forces a full character check, and the expected number of collisions in a text of nn characters at a modulus qq is (nm+1)/q(n - m + 1)/q — the dashed line. The measured counts follow it across three orders of magnitude. One point does not, and it is not an outlier in the statistical sense: at q=257q = 257, one more than the base, the hash stops behaving like a hash.

The roll, and the sign nobody expects

The update itself is three operations and one of them is a trap.

Leaving the window is the character at position ii, whose contribution to the hash is cibm1c_i \cdot b^{m-1}. Arriving is the character at i+mi + m, whose contribution is ci+mc_{i+m}. So

h=((hcibm1)b+ci+m)modqh' = \big((h - c_i \cdot b^{m-1}) \cdot b + c_{i+m}\big) \bmod q

with bm1modqb^{m-1} \bmod q precomputed once. Constant time, no dependence on mm, and the reason the whole algorithm is one pass.

The trap is that hcibm1h - c_i \cdot b^{m-1} can be negative, and the remainder operator in most languages — including the one this site’s libraries are written in — returns a negative remainder for a negative left operand. A hash of 37-37 then never equals the pattern’s hash of 972972, so the algorithm silently finds nothing at some positions and everything continues to look correct: no crash, no assertion, a plausible number of occurrences, and a few of them missing.

The string counter writes the correction out as ((th % mod) + mod) % mod rather than relying on the values staying positive, and the gate’s assertEveryMatcherAgrees is what would catch it — four algorithms are run on the same input and required to return identical occurrence lists, because a matcher that quietly misses an occurrence looks better on every counter. That is the direction that makes agreement worth checking rather than assuming.

Two counters, because there are two costs

The string counter records Rabin–Karp with hashes and verifications kept apart, and the separation is the whole of the analysis rather than a convenience.

Hashes are the rolling updates: one per window, unavoidable, nm+1n - m + 1 of them and never more or fewer. On a 65,536-character text with a sixteen-character pattern that is 65,521, at every modulus, on every input. It is a constant of the algorithm.

Verifications are the character-by-character checks a collision forces. This is the variable, it is where every interesting property of the algorithm lives, and it is what the closed form predicts.

Reporting one number for both would hide exactly the term that varies, which is the same reason KMP’s preprocessing is counted separately from its search and the reason a Bloom filter’s probes are counted separately from its false positives.

The closed form, and a check on it

If the hash spreads windows uniformly over qq residues, a window that is not an occurrence collides with probability 1/q1/q, and the expected number of spurious verifications is (nm+1)/q(n - m + 1)/q.

That is a prediction with a closed form, which on this site means it gets checked against a run rather than quoted. Across the sweep, at n=65,536n = 65{,}536 and m=16m = 16:

  • q=101q = 101: predicted 648.7, measured 675
  • q=509q = 509: predicted 128.7, measured 137
  • q=1,009q = 1{,}009: predicted 64.9, measured 79
  • q=4,093q = 4{,}093: predicted 16.0, measured 22

The agreement is good and it is not perfect, and the direction of the discrepancy is consistent — measured is always a little above predicted. That is the same finite-size effect the probe formula nobody checks documents for open addressing: the text’s windows are not independent draws, because consecutive windows share m1m - 1 characters, so the collisions are slightly correlated and slightly more numerous than a model of independent draws expects.

At the large end the counts become too small to say anything — one or two verifications, where the prediction is 4.0 or 1.0 — and the honest response is to stop asserting agreement there rather than to claim it. The generator does two things about it. It checks the closed form only where the prediction exceeds four, and the tolerance it checks against shrinks as the prediction grows: 0.25+3/expected0.25 + 3/\sqrt{\text{expected}}, which is three standard deviations of Poisson noise plus a quarter for the window correlation above. A flat tolerance passed the long text and failed a shorter one whose numbers were perfectly ordinary, which is the measurement being right and the check being wrong.

What a verification actually costs

The number of verifications is not the cost. What matters is the number multiplied by how long each one runs.

At q=101q = 101 there were 675 verifications and 731 character comparisons in total. That is 1.08 characters per verification. A colliding window on random text is a window whose characters are unrelated to the pattern’s, so the check fails on the first character essentially always, and 675 collisions cost 731 characters rather than 675×16=10,800675 \times 16 = 10{,}800.

A collision is cheap when the colliding string is unrelated to the pattern. That sentence contains the whole of the attack in the second half of this essay, because an adversary’s job is to make it false.

Characters examined searching 20,000 for a pattern of 8random over 26 symbols. Every matcher returns the same 1 occurrence; what differs is what it read to get there. Rabin–Karp examined 8 characters, which is 0.00 per character of text. The naive scan examined 20,862.characters examinedNaive scan20,8621.04 per text characterKnuth–Morris–Pratt20,8331.04 per text characterBoyer–Moore–Horspool2,9850.15 per text characterRabin–Karp80.00 per text characterone unit = one character comparison2607.8× between best and worst
Fig. 2 All four matchers on ordinary text. Rabin–Karp’s character count is the lowest of the four by a wide margin — it examines almost no characters at all — and the figure is misleading unless read with the arithmetic in mind, because the character comparisons are not what it spends. It spends 19,993 rolling-hash updates, which this counter does not draw and which are the actual work.

That figure needs its qualification stated plainly. Rabin–Karp’s character count is near zero, and it is not a sublinear algorithm in any sense: it touches every character of the text twice, once entering the window and once leaving it. The counter that would rank it fairly against Horspool is a counter of arithmetic operations, and this site does not have one.

What the character counter is good for here is the thing it was built for — showing that verification cost is a separate resource that can be driven up independently of everything else.

The modulus is not a free choice

Verifications against the modulus, measured and predictedA hash collision costs Rabin–Karp a full character-by-character check, and the expected number of them is n/q for a modulus q — the dashed line. The measured counts sit on it across three orders of magnitude, and one does not: at q = 257, one more than the base, the rolling hash becomes an alternating sum and verifies 13.6 times more often than predicted. The formula assumes the hash spreads and says nothing about a modulus chosen without reference to the base — nor about a text chosen with reference to both, which costs 15 of the pattern's 8 characters at every one of 6% of its windows.1,00010,00011010010³modulus qverifications in one searchq = 257: 868measuredn / qchosen text: 6%one unit = one character comparison · base 2568 colliding blocks at q = 1,009, none at q = 1,000,003
Fig. 3 The same experiment on a narrower alphabet and a shorter pattern. The formula does not mention the alphabet and the measurements do not need it to: the collision rate is set by the modulus, not by what the characters are. The marked point at q=257q = 257 misbehaves here exactly as it does above, and for the same arithmetic reason.

At q=257q = 257 the measured verifications are 693 against a predicted 254.9 — a factor of 2.7, at a modulus that is prime, is comfortably larger than the alphabet, and would pass any casual inspection.

The reason is arithmetic and it is not subtle once seen. The base is 256 and 2561(mod257)256 \equiv -1 \pmod{257}. So the hash

h=c0bm1+c1bm2++cm1(modq)h = c_0 b^{m-1} + c_1 b^{m-2} + \cdots + c_{m-1} \pmod q

collapses to an alternating sum of the window’s characters: c0c1+c2c_0 - c_1 + c_2 - \cdots. For a sixteen-character window of letters that quantity ranges over a few hundred values rather than over 257 residues uniformly, and — worse — it is unchanged by swapping any two characters two positions apart.

A modulus chosen for being prime, with no reference to the base, gets this. The rule that follows is not “use a big modulus”; it is that qq must not be close to a power of the base, and the failure is silent: every count is still correct, every occurrence is still found, and the algorithm is a constant factor slower in a way no test would report.

This is the third time on this site that a hash has failed because of a relationship between two constants nobody wrote down together — the probe formula and a hash is a family, not a function are the other two — and the pattern is the same each time. The analysis assumes uniformity; the implementation has structure; nothing checks.

Building the collisions on purpose

The closed form assumes the text was not chosen with the hash in mind. The adversary who knows the seed makes the general argument: a randomised algorithm whose randomness is public is not randomised, and a hash whose parameters are in the source is public.

The construction here is elementary. To make a verification expensive it is not enough that a block collides — a colliding block that differs from the pattern immediately costs one character comparison. What is needed is a block that collides and agrees with the pattern for a long way, so that the check runs almost to the end before failing.

Take the pattern’s first m2m-2 characters and search over the last two. Two such blocks have hashes differing by d1256+d2d_1 \cdot 256 + d_2 where each dd is a character difference, so the difference is at most 65,535 in absolute value. Whether a collision exists at all is therefore decided by one comparison:

At q=1,009q = 1{,}009 the construction finds 8 distinct colliding blocks, against an expectation of 942/1009=8.7694^2/1009 = 8.76 from enumerating printable pairs — agreement close enough to say the search space is being explored as modelled.

At q=1,000,003q = 1{,}000{,}003 it finds none, and cannot. The hash difference cannot reach a modulus larger than 65,536, so no two-character variation of the pattern collides with it. The attack is not merely hard there; it is arithmetically impossible for this construction.

That is a considerably sharper statement than the usual advice, and it is checkable rather than reassuring. The gate asserts both halves — that the collisions exist at the small modulus and that they do not exist at the large one — because a construction that quietly succeeded everywhere would be measuring something other than what it claims.

Two more sweeps say what the defect is a property of, and neither of them is the text. Double the pattern and the collision rate at a good modulus halves, exactly as n/qn/q says it should; the degenerate modulus stays degenerate and its overshoot falls with it, because the alternating sum has fewer terms to cancel.

Verifications against the modulus, measured and predictedA hash collision costs Rabin–Karp a full character-by-character check, and the expected number of them is n/q for a modulus q — the dashed line. The measured counts sit on it across three orders of magnitude, and one does not: at q = 257, one more than the base, the rolling hash becomes an alternating sum and verifies 2.4 times more often than predicted. The formula assumes the hash spreads and says nothing about a modulus chosen without reference to the base — nor about a text chosen with reference to both, which costs 15 of the pattern's 32 characters at every one of 6% of its windows.1,00010,000110100modulus qverifications in one searchq = 257: 604measuredn / qchosen text: 6%one unit = one character comparison · base 2568 colliding blocks at q = 1,009, none at q = 1,000,003
Fig. 4 The same six moduli with a pattern of thirty-two characters rather than sixteen. The measured counts still sit on the n/qn/q line across three orders of magnitude, and q=257q = 257 still does not — 2.4× over rather than 51.3×, because the constructed text’s blocks are now half the pattern rather than the whole of it. What does not change is that one modulus in six is off the line.

And narrowing the alphabet does not rescue it either, which is the case a reader might expect to be safe because a small alphabet makes every window’s hash more likely to collide anyway.

Verifications against the modulus, measured and predictedA hash collision costs Rabin–Karp a full character-by-character check, and the expected number of them is n/q for a modulus q — the dashed line. The measured counts sit on it across three orders of magnitude, and one does not: at q = 257, one more than the base, the rolling hash becomes an alternating sum and verifies 51.3 times more often than predicted. The formula assumes the hash spreads and says nothing about a modulus chosen without reference to the base — nor about a text chosen with reference to both, which costs 15 of the pattern's 16 characters at every one of 6% of its windows.1,00010,00011010010³modulus qverifications in one searchq = 257: 3266measuredn / qchosen text: 6%one unit = one character comparison · base 2568 colliding blocks at q = 1,009, none at q = 1,000,003
Fig. 5 Two symbols rather than twenty-six, over a quarter of the text. The prediction holds at every modulus but the degenerate one, which verifies 51.3 times more often than n/qn/q says — the same factor as on the wide alphabet, because the failure is arithmetic between the base and the modulus and has nothing to do with what the characters are.

Nor is it a property of the six moduli that happen to be on the plate. Six different primes, the same base, and the same one modulus is off the line — because the one that is off is not one of the six.

Verifications against the modulus, measured and predictedA hash collision costs Rabin–Karp a full character-by-character check, and the expected number of them is n/q for a modulus q — the dashed line. The measured counts sit on it across three orders of magnitude, and one does not: at q = 257, one more than the base, the rolling hash becomes an alternating sum and verifies 2.7 times more often than predicted. The formula assumes the hash spreads and says nothing about a modulus chosen without reference to the base — nor about a text chosen with reference to both, which costs 15 of the pattern's 16 characters at every one of 6% of its windows.1001,00010,000110100modulus qverifications in one searchq = 257: 693measuredn / qchosen text: 6%one unit = one character comparison · base 2568 colliding blocks at q = 1,009, none at q = 1,000,003
Fig. 6 A different sweep of primes — 97 to 98,317 — against the same n/qn/q line. Every one of them sits on it, and q=257q = 257, which is one more than the base and is not a member of either sweep, verifies 2.7 times more often than predicted. The line is a property of the formula and the outlier is a property of the arithmetic.

What the attack costs the algorithm

Cycling those 8 blocks into a text of 1,024 characters and searching it for the pattern:

  • 1,009 windows examined
  • 64 verifications, one at each aligned block
  • 960 character comparisons, exactly 15 per verification
  • 0 occurrences found

Fifteen of the pattern’s sixteen characters, every time, on a text where the pattern does not appear. Against the random-text baseline at the same modulus, the same length of text would expect one verification costing about one character. The attack has multiplied the verification cost by roughly a thousand while leaving the hash count, the answer, and every other measurable property untouched.

Scaled up, this is Θ(nm)\Theta(nm): the algorithm’s worst case, reached on an input a page of arithmetic produces, at a modulus that looks fine.

Characters examined searching 4,096 for a pattern of 16one repeated symbol, pattern present everywhere. Every matcher returns the same 4081 occurrences; what differs is what it read to get there. Knuth–Morris–Pratt examined 4,096 characters, which is 1.00 per character of text. The naive scan examined 65,296.characters examinedNaive scan65,29615.94 per text characterKnuth–Morris–Pratt4,0961.00 per text characterBoyer–Moore–Horspool65,29615.94 per text characterRabin–Karp65,29615.94 per text characterone unit = one character comparison15.9× between best and worst
Fig. 7 The worst case that needs no adversary at all: a text of one repeated character searched for sixteen of the same. Every window collides because every window is identical, every verification runs the full pattern and succeeds, and Rabin–Karp examines 65,296 characters of a 4,096-character text. Nothing is wrong here — the pattern really does occur 4,081 times, and the work is the answer rather than waste.

An accident that behaves like an attack

One further measurement, found while writing the check rather than while writing the essay.

The first version of the adversary used abababababababab as the pattern, and its verification cost came out at 8.01 characters rather than the expected 15 — and 505 of 1,009 windows verified rather than 64.

The pattern is periodic, and a periodic pattern collides with itself under a shift. Shifting abababababababab by two characters gives back the same string, so a window offset by two from a colliding block collides too, and its verification fails after 13 characters instead of 15; offset four fails after 11, and so on down. Half the windows verify, at costs from 15 down to 1, and the mean is 8.

Nothing was wrong with the construction. The victim was wrong, and the assertion that caught it was the one demanding the cost per verification be near mm. That is the third time in this site’s history that an assertion has rejected the code building the case rather than the code being measured, and it is why the check reads the way it does now, with the choice of a non-periodic pattern justified in a comment above it.

It is also a real property worth keeping: a periodic pattern is its own partial adversary, and it needs no attacker.

The counter that does not exist, estimated anyway

The qualification above says the character counter cannot rank this algorithm fairly and that no counter of arithmetic operations exists here. That is true, and the estimate is worth making anyway, because the gap turns out to be large enough that no plausible accounting closes it — and because the direction is the opposite of what the plate shows.

Count a memory read and an arithmetic operation as one unit each, on the 20,000-character text with m=8m = 8 drawn above.

Rabin–Karp performs 19,993 rolling updates. Each is a subtract, a multiply, an add and a remainder, plus the add and second remainder the sign correction requires — six, and the hash comparison at each window makes seven. That is about 140,000 units, and essentially none of them are the character comparisons the counter draws.

Horspool examines about a seventh of the characters at this pattern length, so roughly 2,857 alignments. Each is a shift-table lookup and a comparison, with the shift arithmetic on top — call it three. About 8,600 units.

So the honest ranking on ordinary text is Horspool ahead by something between ten and twenty times, on the algorithm the character plate shows as spending almost nothing. The plate is not merely incomplete about Rabin–Karp; read as a ranking it is inverted, which is a stronger criticism of it and the reason the qualification is repeated wherever the plate appears.

Two things sharpen it further and neither needs a counter.

The remainder is not one unit. Integer division is the slowest arithmetic instruction on ordinary hardware by a wide margin, typically tens of times an add’s latency, and there are two per update. Any weighting that reflects that widens the gap rather than closing it, so the estimate above is generous to the algorithm it is against.

And the gap grows with the pattern. Rabin–Karp’s cost is flat in mm — that is what the sweep plate shows, and it is the algorithm’s genuine distinguishing property. Horspool’s falls with mm, because a longer pattern shifts further. So the two never cross as the pattern lengthens; the algorithm that ignores mm loses to the one that exploits it, by more at every size.

Which locates the crossing where it actually is, and it is not on this axis at all. Searching for kk patterns costs Horspool kk passes — about 8,600 units each — while Rabin–Karp keeps its 140,000 and adds one set lookup per window. Setting those equal puts the crossing at about twenty patterns, and past it the single-pass structure wins by the full factor of kk.

That is the quantitative form of the claim the shift the pattern already knows leaves implicit, and it is a much narrower recommendation than “use a rolling hash”. For one pattern on ordinary text, do not: an algorithm reading a seventh of the characters beats one doing arithmetic on all of them. For twenty or more, do, and the reason is the same one that makes a filter allowed to be wrong worth its false positives — a cheap test applied once to each position, with the expensive test reserved for what survives.

The estimate is an estimate and is labelled as one. What makes it worth writing down rather than deferring to a counter nobody has built is that its conclusion is a factor of ten with a stated model, and a measurement disagreeing with it by less than that would not change any of the advice above.

Where this leaves the four algorithms

Rabin–Karp is the only one of the four whose cost is a distribution rather than a number, and that is its whole character. The other three are deterministic: given a text and a pattern, their counts are fixed, and the interesting question is which inputs are bad. Here the counts depend on a modulus chosen by the implementer, and the interesting question is what the implementer assumed about the text.

Expected is not average is the essay that draws the distinction this rests on, and it applies exactly. The n/qn/q formula is an expectation over an assumption about the input, not a guarantee over inputs — so it says nothing at all about a text somebody built, and it does not say what a real hash on real text will do either, only what an idealised one would.

What the algorithm is genuinely good at, and what nothing here has measured, is the case the other three cannot do: many patterns at once. Hash all kk patterns, put the values in a set, roll one hash along the text, and look each window up. The cost is one pass and kk lookups’ worth of memory, against kk passes for anything else.

That structure should look familiar. A cheap probabilistic filter in front of an expensive exact check, where the filter may say yes wrongly and never says no wrongly, is precisely a filter that is allowed to be wrong — and the two share more than a shape. Both have a false-positive rate with a closed form, both spend the saved work on verifying the positives, and both are undone by the same thing, which is somebody choosing the input with the hash in hand. The Bloom filter’s answer to that is a hash family chosen at run time; Rabin–Karp’s is a modulus chosen at run time, and almost no implementation does it.

The other setting where the rolling hash is the whole idea is one where there is no pattern at all. Content-defined chunking rolls a hash along a file and cuts a chunk boundary wherever the value has some number of low bits zero. Insert a byte at the front of the file and every boundary after the insertion is in the same place as before, because each is decided by the last few bytes rather than by an offset — which is what makes deduplicating storage and incremental sync work, and which a fixed-size chunker cannot do at all. Nothing is being matched; the only property being used is that the summary rolls.

That is the honest place for this algorithm in the collection. As a substring search it is beaten on ordinary text by an algorithm that reads a seventh of the characters, and it is the only one of the four whose worst case an outsider can arrange. As a way of summarising every window of a stream cheaply, it has no competition among the other three, because none of them produces a summary at all.

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.

Adversarial inputClosed formExpected costHash collisionModulusPattern matchingRandomised algorithmRolling hashVerification