The Boyer-Moore Fast String Searching Algorithm
cs.utexas.edu
cs.utexas.edu
1. How effective it was (and is). Both the demonstrations here (of Boyer-Moore algorithm, and of Knuth-Morris-Pratt) illustrate the algorithms very clearly (the same for the linear-time majority vote algorithm); the interactive (web) medium really helps (just hyperlinks and maybe the back button); and the technology is nothing more than a hand-coded HTML file (with abundant "<BR>"s), already over a decade old when I saw it. (The DOCTYPE line in the source mentioning "HTML 3.2" lets us date this to 1997 I guess, as HTML 3.2 was released in January 1997 and HTML 4 in December 1997.)
2. The page one level up from this (https://www.cs.utexas.edu/users/moore/best-ideas/index.html), titled ``My'' Best Ideas, which opens [Incidentally, this usage of `` and '' reflects ancient fonts—you can see the same usage in Emacs, TeX, etc—explained well by Markus Kuhn here: https://www.cl.cam.ac.uk/~mgk25/ucs/quotes.html] with:
> Here are the technical achievements of which I am most proud. I think such a list gives more insight into my research than, say, the traditional ``Honors and Awards'' section of a resume.
And it lists eight ideas. Firstly, what a great idea to make such a list. And secondly, an impressive research career, and it's a successful life, to end with a portfolio of 8 ideas of which one can be proud. One doesn't need much more, and to have a handful of good ideas is something to hope for. I'm reminded of this, from Proofs from the Book (saw it here: https://sites.math.rutgers.edu/~zeilberg/Opinion82.html ):
> The essence of mathematics is proving theorems – and so, that is what mathematicians do: they prove theorems. But to tell the truth, what they really want to prove once in their lifetime, is a Lemma…
Not necessarily. HTML 4 was a big change from 3.2, more complicated, and stricter about separating structure from presentation. Lots of people stayed with the more familiar 3.2 for a long time.
In fact recently I was wondering why markupsafe generates numeric entities for the quote characters.
That was added to Jinja (before markupsafe was moved out), for HTML 3.2 compatibility, because 3.2 only had 3 named entities (amp, gt, and lt).
According to jinja’s log, this was fixed in 2008. Sadly no link to anything was recorded, but it means either mitsuhiko was feeling playful (and happened to remember this issue) or someone was generating HTML 3.2, with jinja, in 2008.
But TIL after reading this page that there's a new variation optimized for small alphabets such as DNA: https://www.cs.utexas.edu/users/moore/publications/sustik-mo...
This page also links to a demonstration of how the search works which I found easier to follow than the wiki page on the algorithm.
The one you linked to maintains state during the search loop to get bigger shifts, and complexity in search loops always slows it down, sometimes to the point there is a net loss.
If the needle was reasonably long, which is not uncommon for low alphabet needles, I suspect a better approach would be to use a hash based Horspool style algorithm working on n-grams, say 4.
This works by calculating shifts based on ngrams, rather than individual bytes. Since there are many more permutations, you effectively increase the alphabet and hence the shifts also increase. At the cost of always having to read an entire ngram before you can shift.
Edit: you also hash the ngram into a smaller hash table of course, you don't maintain a shift position for each ngram. You can tune the size of the hash table for lower memory or higher performance. This is similar to the multi pattern Wu Manber search, but applied to a single needle.
Edit 2: I have found this algorithm also beats Horspool most of the time, say for an ngram size of 2 or 3, on any alphabet, not just low ones.
While the algorithm is time linear (Boyer-Moore is sub linear), it ends up being significantly faster on textual content as the per character operations are significantly simplier.
Btw. digital tablet with pen input is gold thinking about algorithms, as you get modifying your scenarios in place endlessly and copy and paste over paper + pencil.
It may have been on an HP9000s2xx machine instead of Amiga.
Taught me that hand optimized assembly can beat clever algorithms.
Memory is a block device now. DDR delivers 8 bytes per transaction per channel, e.g. 16 bytes per transaction for dual-channel memory configuration. Skipping bytes delivers little profit in terms of memory bandwidth (but it saves other things, CPUs have very small limit how many memory load instructions can run per cycle).
SIMD instructions can compare vectors for about the same cost as scalars, e.g. _mm_cmpeq_epi8 (SSE2) and vceqq_u8 (NEON) compare 16 bytes with another 16 bytes.
If I wanted a super-fast substring search, I would try to leverage these things.
It can still be used to find a substring of 100 64-bit values in a much longer 64-bit string, but ... it’s a problem I never encountered (and only encountered a 16-bit version of it once in my life). Then again, if you use 32-bit for each Unicode character and have a lot of emojis, it might still be somewhat competitive with brute force.
There are consequences to the proportion of some nucleotides, the most well-known being the GC-content[1], or proportion in a sequence of bases being G or C (G pairs with C and A pairs with T). There are benefits to a higher GC-content, like the sequence being more stable.
From this the obvious question would be why this matters, since you obviously can't replace an A-T pair with a G-C pair just to increase this proportion. Except that there are ways to do it… kind of. If you look at the codon table[2], you'll see that there are multiple ways to represent each protein encoded by a triplet of nucleotides.
I love how the DNA language, much unlike spoke words, is in close relation to physical properties of the world.
There really is some exciting magic in the interception of biology and computation.
For GCC it would seem that using this built in pseudo function call does the trick;
__builtin_prefetch((void *)p,0,0);
Related TIL: ripgrep ̶d̶o̶e̶s̶n̶'̶t̶ edit mostly doesn't use Boyer-Moore. [0]
[0] https://github.com/BurntSushi/ripgrep/issues/493#issuecommen...
Note though that it is going away soon. It's going to be replaced with a combination of Two-Way, Rabin-Karp and a fast vectorized prefilter based on this: http://0x80.pl/articles/simd-strfind.html#algorithm-1-generi...
The "secret sauce" is that in the vectorized prefilter, we don't just choose the first and last bytes. Instead, we pick bytes that we think will be rare based on a priori assumption of relative byte frequencies.
But there are many queries that are not so fast today. For example:
$ time rg-master -Nc 'strengths' OpenSubtitles2018.raw.sample.en
242
real 0.255
user 0.204
sys 0.050
maxmem 903 MB
faults 0
$ time rg -Nc 'strengths' OpenSubtitles2018.raw.sample.en
242
real 0.092
user 0.068
sys 0.024
maxmem 904 MB
faults 0
Where the second command has the new substring algorithm. Nothing earth shattering to be honest. ripgrep already did a good job of dealing with common cases.This is ripgrep's current work-horse: https://github.com/rust-lang/regex/blob/3db8722d0b204a85380f...
The revised algorithm is based on the same principles (heuristics derived from an a priori frequency distribution), but shouldn't be as easy to get into cases where it runs very slowly. Here's another more poignant example (where the needle is two ASCII space characters):
$ time rg-master -Nc ' ' OpenSubtitles2018.raw.sample.en
real 0.887
user 0.859
sys 0.027
maxmem 903 MB
faults 0
$ time rg -Nc ' ' OpenSubtitles2018.raw.sample.en
real 0.084
user 0.043
sys 0.040
maxmem 903 MB
faults 0How did you come up with the frequency distribution? English text? Code samples? Languages other than English?
https://github.com/rust-lang/regex/blob/3db8722d0b204a85380f...
Since rg (which is awesome) understands which programming languages map to which file suffixes, could it make sense to do per-language frequency analysis? {}[] are common in C-style languages, but much less so in lisp, python, etc.
On top of that, it's unlikely to make much of a difference. Not only would the frequency distributions between languages look pretty similar, but it's still just a guess at the end of the day. There is such a thing as overfitting too. :-)
The grep tools do track line endings, yes. ripgrep does it by default where as GNU grep does not. Line endings are only tracked to print line numbers. If you don't need to print line numbers, then you don't need to track all line endings.
The thing with tiny needles is that they aren't all created equal. If your tiny needle is a single byte and that byte doesn't occur too frequently, then it's going to be very fast to find occurrences of that byte.
And I realize newlines is only to track line counts. Am I wrong that that slows things down?
Yes. That's what memchr does: https://github.com/BurntSushi/rust-memchr/blob/427fdc384007d...
> And I realize newlines is only to track line counts. Am I wrong that that slows things down?
It does, but only a little if it's implemented using vector instructions. You can try with ripgrep using -N/-n. GNU grep's line counting isn't vectorized IIRC, so you might notice bigger differences there.
[1] bytes/lines on "wc -l *.[ch]" in the ffmpeg/libavformat directory I happened to have open says there's a newline every ~34 bytes.
edit: I suppose if ripgrep -N and ripgrep -n aren't that far apart, it's fast enough already. But I have to think it's not getting much mileage out of that 64-bytes-at-a-time loop.
I'm not sure what GNU grep/wc does, but assuming you're getting line numbers for relatively rare matches, you probably shouldn't use memchr. Instead, load chunks of bytes into a SIMD/vector register, compare each one for equality to 0A, take the popcnt, and add it to a line number counter. You don't actually care where (most of) the newlines are located, just how many of them there are.
I believe GNU grep just uses memchr for this. But I might be mis-remembering.
However, memchr is still used for finding the bounds of a line when a match occurs. memchr still does pretty well on shorter haystacks too. The main loop is indeed quite large, but before that, it does unaligned loads of 32 bytes: https://github.com/BurntSushi/rust-memchr/blob/427fdc384007d...
Or rather nothing will change, once the pattern length exceeded the expected value for the alphabet used. E.g. for a simplified DNA alphabet, extended BC shifts on average 4 characters. On the fly I can't do the stochastics on GS, but I assume substring match length and chance of reoccurrence run against each other in a similar manner.
On the other hand long patterns increase preprocessing/access time: Extended BC and GS have O(m) for preprocessing, and BC may have non-constant access, depending on space tradeoff.
I mean yes of course in specific scenarios certain assumptions can be made. E.g. for UUIDs you probably better off exploiting the fixed length pattern too.
But for the general/theoretical case these intuitions do not pan out usually. I mean even "memory hacks" make assumptions about the data indirectly by common hardware architecture.
That is, most data isn't random strings of course XD
However when BM is used in e.g. bioinformatics on DNA and you are still stuck with firstly finding data against evolutionary noise, when your algorithms must adapt to your hypotheses, theory becomes more relevant I assume.
I think DNA focused "string search problems" are really inspiring and have a lot of potential for mingling with philosophical fundamentals of informatics. There is something about the evolutionary emergence of "data" AFAIK no other field offers. E.g. the overlapping, extending, or contextualizing information meta-layers upon meta-layers in DNA translation and structure "specified" to no more than merely exist. Throwing Boyer-Moore at ASCII encoded sequences in FASTA files almost feels blasphemous, or the arrogantly fallacious human essence.
I work on text search where the alphabet size is 256. If someone told me to work on text search where the alphabet size is 4, a lot of what I'd do would change, starting with my benchmark inputs.
So I kind of think responding to general claims about substring performance with, "well in DNA searching..." is kind of reframing the discussion to a specialized use case with very different priors. That is, I'm not sure anyone ever said running BM on DNA was a good idea. But maybe it's convenient, so that's what folks do. :-)
As I recognized, introducing assumptions of course opens possibilities for doing things differently.
I was using the DNA example, because bioinformatics is where a lot of people learn about BMA, because the math is easy there and we are not tempted to make assumptions about the text, even tho it's not random. For most bioinfo applications the alphabet is not four letters, but e.g. the amino acid alphabet, or having additional sequence information included.
In bioinfo practice the naive algorithm often performs competitive against BMA if you optimize for hardware assumptions. Branch prediction is a bitch for BMA.
It's the optimal solution to https://leetcode.com/problems/majority-element/ – as in it's required to solve the problem in linear time and in O(1) space.
From the paper's abstract:
> the algorithm uses storage in a way that permits an efficient use of magnetic tape
I like to use Leetcode hard or medium problems as interesting puzzles to work on in my head when I've got some time to kill, such as waiting in line for some service or lying in bed trying to fall asleep.
One of those problems was given an input array of integers, find the smallest positive integer that is not in the array. They wanted this in O(N) time and O(1) space.
I spent something like two months trying to solve that. I even spent a fair bit of that time trying to prove that it could not be done, hoping that maybe if I could not prove it impossible I might get some insights from why I could not do so that would help me figure out how to do it.
That "try to disprove it to gain insight" approach didn't work, because my attempts to disprove it actually almost convinced me that it was in fact impossible. I say almost, because my argument using Turing machines actually seemed like it wa right, but it has been around 40 years since I last studied or used Turing machines, and so I wasn't quite sure that I wasn't missing something.
I finally gave up, and took a quick glance at a published solution, trying to just see enough to get unstuck. The first thing I saw was that they were writing to the input array. As soon as I saw that you can overwrite the input, and so by O(1) space what they really mean is O(1) additional space besides this array of length N that we give you, the solution came quickly.
When researching string search algorithms some time ago I came across max shift Boyer moore which seemed to be a very nice incremental improvement.
I think it is just that Horspool works better on modern hardware. BM isn't nearly as much of a gain over the naive search that it was 25 years ago.
Edit: the max shift change I mentioned above can be done to Horspool as well!
For very short needles though (e.g. less than 8) it is hard to beat the SHIFT OR algorithm.
Even though it is not sublinear, looking at every position, it tracks matches using a bitmask. In practice this is very fast and has very good cache hits. It easily beats BM or Horspool by quite a margin. They rely for speed in skips, and you don't get great skips with a short needle.
One of my favorite things about it is that it has a "phase change" graph at the end of each chapter, saying for each combination of alphabet size and pattern size, which algorithm fares better. It also favors algorithms that are simple to implement (such as Horspool instead of Boyer-Moore), because those also tend to be the fastest.
But the fastest is currently EPSM, by S. Faro and O. M. Kulekci. See https://smart-tool.github.io/smart/
"Exact Packed String Matching" optimized for SIMD SSE4.2/AVX (x86_64 and aarch64). It performs stable and best on all sizes.
The site I linked to compares 199 fast string search algorithms, with the usual ones (BM, KMP, BMH) being pretty slow. EPSM outperforms all the others being mentioned here on these platforms. It's also the latest.
And yes, EPSM outperforms the others by a mile.
If you are comfortable with your search performance being bound to particular CPU architectures, and that may be entirely reasonable, then you can't beat that approach.
start:
xor ah,ah
lea bx, masktable
mov cx, textlength
lea si, texttosearch
test_character:
or ah, startbit (set bit 8-N of the ah register)
lodsb (get a character into AL)
xlat (AL gets [BX+AL])
and ah,al (masks off non-matching characters)
shl ah,1 (AH=AH*2, carry goes into Carry Flag)
jc match_found ; we found a match, break out
loopnz test_character
no_match_found: ; get here if there isn't a match
In a 64 bit environment, you should be able to search for up to 64 characters fairly quickly, using a table that is 8*256 bytes, you can do case insensitive searches, along with a ? for any character to do some wildcarding. The nice thing is that everything is in the cache until you jump out when you have a match, and not before.It searches about 1 gigabyte of text in 2 seconds.
https://github.com/mikewarot/fastsearch/blob/master/fastsear...
* https://doi.org/10.1016/S0167-6423(03)00013-3 (PDF is freely available)
I've been mulling how to apply fast search algorithms to regular expressions for a while. There isn't really one technique that works well for all expressions, so the more options right now the better. Eventually I'll settle on a few that offer the best overall coverage and performance.
https://github.com/sarpdag/boyermoore
Go's strings package depends on the substring size tries different approaches like Rabin Karp algorithm, (https://en.wikipedia.org/wiki/Rabin%E2%80%93Karp_algorithm)
Such an eye opener when I learned about it the first time! And a reminder that we really stand on the shoulder of giants.
> Like the Aho–Corasick string matching algorithm, it can search for multiple patterns at once. It combines ideas from Aho–Corasick with the fast matching of the Boyer–Moore string search algorithm. For a text of length n and maximum pattern length of m, its worst-case running time is O(m·n), though the average case is often much better.
> GNU grep implements a string matching algorithm very similar to Commentz-Walter.
I assume that even with a naieve search a modern CPU can search at the memory read speed.
Without any kind of indexing beforehand it is impossible to search faster than the memory read speed.
The approach shown of only checking some characters doesn't help remove memory bandwidth constraints unless the thing you are looking for is very long (because cache lines are very wide), which I suspect is not a common use case.
T: aaaaaaaaaaaa
P: xaaaaa
Not easy to come up with an improvement, there will always be that exception you didn't think of XD
I wonder, if you could derive meta information about the DNA "lyrical" composition by observing the BM put through. After all the algorithm assumes random text.
I haven't recently looked into the sub-quadratic worst case improved version of BM, but IIRC it's far from trivial and not what most people think of for this algorithm. I assume it's also slower IRL, for cache misses, or for expensive operations like modulo in the main loop, and prolly has a space penalty.
BM just has a better average case than naive string search.