Repository navigation
Conversation
|
@khwilliamson can you check out the bitmap for folded data please. I am not sure i used the inverted fold data properly. |
8319656 to
5c5f7ed
Compare
|
At first glance, on my dev VM.
I'll start going through the commits in a bit. |
Scrap that, I mustn't have had the latest updates from this branch. |
5c5f7ed to
e910fa1
Compare
FWIW, this is the kind of benchmark I personally try to avoid. It doesn't do any work, so the timing is very artificial. Often when you put some kind of sane workload in the loop all of a sudden the 10% increase is a negligible increase because the work in the body swamps everything else. |
|
S_regmatch comments at Line 7318 in e910fa1 say that: Could some of the same mechanisms introduced in this PR apply there too? |
e910fa1 to
ed58fb5
Compare
|
Not sure if I'm understanding the implementation correctly, but I've got two main concerns that I'm still feeling out:
|
|
Oh, I see that the interplay between BMH and start-octet search isn't quite what I'd originally pictured for UTF-8, so it's not as bad as all that. |
99d176c to
3e6820c
Compare
|
@richardleach, following up on your last comment. So, a bit of context for those not familiar with it. Boyer–Moore–Horspool matching is a variant of Boyer–Moore matching. The usual description searches for one string. We use a version adapted to finding positions where one of a set of strings might match. It is easiest to picture with words of the same length. Suppose we have a bunch of six-octet words. BMH looks at the octet where a word would end, and uses a table to decide how far to advance. An entry of zero says “a word might end here, check further”. An entry of one says “advance one octet and check again”. An octet which appears nowhere in our words can have an entry of six, allowing us to skip six octets. With varying word lengths, we use a common prefix length determined by the shortest word, subject to the table’s size limit. So the anchor is not necessarily the last octet of a longer word. We build separate BMH tables for native and UTF-8 targets. We combine this with a check of the possible first octet. If the end-position check looks promising but the first octet cannot start any word, we can reject that position without entering Aho–Corasick proper. The repeat scanner handles another part of this process. After checking a machine-word-sized window and finding no possible starts, we know that identical following windows cannot contain a start either. We can then chew through repeated data a machine word at a time. This is useful for long runs of spaces, where repeatedly consulting the BMH table can cost more than comparing and skipping a whole word. Your point about odd-sized repeats led to another improvement. The scanner now handles periods of 3, 5, 6 and 7 octets as well. The trick is to compare enough machine words that the repeating sequence returns to the same position at each block boundary. On a 64-bit build, a three-octet repeat lines up again after three machine words, or 24 octets. This is particularly useful for Unicode whitespace, since many Unicode spaces occupy three UTF-8 octets. These shortcuts apply when AC is at its root, with no partial word in progress. Once matching has started, AC transitions and failure links keep track of the possible words. When it returns to the root, the scanning shortcuts become available again. I also tested restricting repeat detection according to the BMH shift, and adding a cheap probe before doing more repeat checks. Both helped some inputs and hurt others, so neither was retained as a general rule. The right choice depends on both the search words and the input. I do not think we have a useful universal cutoff yet. Anyway, here are some numbers from the latest version. Times are arithmetic means in microseconds per benchmark iteration, from twelve interleaved samples with matching compiler settings. Negative percentages mean less time. These compare the complete builds, rather than isolating any one optimisation.
The repeated-input cases have about 32 KiB before a final match. The whitespace cases use actual Unicode spaces. The document cases scan 32,768 characters. Each anchored iteration tries three strings: two successful matches and one failure. The dense case returns 4,096 matches and includes counting them and accumulating their lengths. Input preparation and initial pattern construction are outside the timer. So, for example, scanning three-octet Unicode whitespace fell from 98.47 µs on blead to 2.61 µs here. There are also metrics which have degraded since 5.44. The source-search and anchored Latin-1 cases above all improve with this patch sequence relative to blead, although they have not recovered all the lost ground. I have plans to address the remaining representation costs in a follow-up. Dense matching is a separate remaining case where the new code is still slower. I have worked through the implementation feedback too: removed redundant bounds checks, reduced position tracking, and avoided conversions where the input is already in the representation the trie needs. Some apparently obvious changes consistently lost in measurement, so I left them out. Code layout also proved sensitive: changing how the loop was expressed changed the generated branches enough to matter. Construction costs are separate. Building the folded start maps adds a few microseconds to the small compilation cases we measured. That is paid when constructing the regex, while the scanning improvements apply each time it runs. I think we are now in good territory. Next PR I want to try a hypothetical TRIE V2, with completely separate native and UTF-8 tables and structs, so that we can remove more of the representation-handling work from execution. Tries starting using codepoints, switched to utf8, and now I think they need to do latin-1 codepoints and utf8 octets. Sigh. Suggestions for further improvements are very welcome. |
6d8e90b to
9b5d2c6
Compare
|
Following up on your review: I renamed the to_uni_upper parameter from ncp to uv in the declaration and implementation, and added an explanation of native-Unicode numbering near the start of the generated perlapi documentation. The API introduction now explains the 0..255/native and above-255/Unicode split, when NATIVE_TO_UNI and UNI_TO_NATIVE are useful, and that older names containing uni may use native-Unicode values. |
509a0c0 to
8b13e73
Compare
I've not looked closely in more than a week. Will look this afternoon, but probably not going to be anything from me. (In terms of juicing finding the start position: where 1-4 masks is enough to describe the possible start octet, a |
We can skip much of the input before trying the Aho-Corasick matcher. Build separate Boyer-Moore-Horspool shift tables for native and UTF-8 targets, and a bitmap of the octets which can start a word. The shift tables let us skip ahead, and the bitmap rejects impossible starts. When a whole UV contains no possible start, we can skip identical UVs as well. This handles long runs of repeated characters without checking each octet. We only do this at the root, where no word is in progress. Avoid updating the position ring on failed root transitions, and reject native targets outright when every word needs a codepoint above 255. Partial fix for #24934
For folded words we need a map of the original target characters, not their folded bytes. Use inverse folds to find those characters, including characters whose multi-character folds can start a word. The map may admit extra candidates, as the matcher checks the actual folding rules. Leave both BMH tables empty for folded tries. Offsets in a folded word do not tell us how far we can skip in the original input. We can use the start map and repeat scanner at the root when no encoded or folded input is pending. Resolves #24935
A nonempty folded literal needs an input character. HOP3c can leave the last possible start at the end of a short target, where UTF8_SAFE_SKIP() returns zero. We then retry the same failed match forever. Check that input remains before trying the literal. This can happen on the next /g attempt when prefix extraction leaves a folded start class. The same problem occurs on blead without the AC prefilter changes. Fixes #24934
Once we have checked a whole UV of octets which cannot start a match, an identical UV cannot contain a start either. The BMH shift does not affect that, so we need not restrict the repeat scanner to shifts of zero or one. For /walrus|cat/, A has a shift of three. We used to keep looking up that shift on a long run of A's instead of scanning in UV sized chunks. On Linux x86_64, nonthreaded GCC 13.3 -O2 builds, three scans of 200 million A's followed by cat drop from 0.489s to 0.048s. There is a cost when input does not repeat: 32 scans of 1 MiB of random uppercase ASCII for /abcdefgh|ijklmnop/ rise from 0.0101s to 0.0302s. Establishing the rejected window checks octets which BMH could otherwise skip.
We check both the last possible start and the end of the string for every candidate. Calculate one exclusive limit before the loop instead. Cap it at strend. HOP3c can leave the last possible start at the end of a short target, but a nonempty literal cannot match there. Trying it would leave UTF8_SAFE_SKIP() returning zero without advancing the scan.
For nonfolded native matches, each input byte is one character. We can find the start by subtracting the word length from the current pointer, without recording every input position in a ring. Keep the ring for UTF-8 and folded matches, but wrap its index with a comparison instead of a remainder operation. Calculate where the initial scan reaches a UV boundary once, rather than checking alignment for every rejected octet. The AC construction queue holds each state once, so its indexes need not wrap either.
Use a line length of 100, with a few hand edits for clarity.
We need comparisons which distinguish ASCII, high native bytes and upgraded input, using the same words and text for each Perl build. Add t/re/trie_bench_corpus.pl to provide fixed HTML, prose, Perl-source and FASTA workloads. Add t/re/trie_bench_corpus.t to check that we can recreate the inputs and get the expected matches. Add Porting/bench-trie.pl to run alternating samples across Perl builds and report each workload separately. It checks match spans and AC start classes before timing and records the inputs, build settings and raw samples so we can repeat the comparison and investigate regressions.
Appending to the same list causes needless conflicts. Suggest choosing a section at random when adding cases.
We already have the folder's UTF-8 bytes, so there is no need to decode each character and encode it again. Feed those bytes to the trie directly. Track pending octets separately from pending folded characters so multi-character folds keep the right input positions. For nonfolded UTF-8 AC matches, subtract the word's byte length to find its start. This avoids the position ring and its heap allocation for long words. Folded input still needs the ring, as its length can vary. Once the acceptance chains are built, reuse each word's accept-state slot for its byte length. We no longer need the accept state then, so the word records need not grow.
GH #8267 and GH #16250 have passed since 77af3ea cleared captures when CURLYX chooses zero repetitions. Remove the stale TODO cases. GH #8267 already has ordinary regression coverage. Move GH #16250 to re_tests, keeping the original inputs and covering both assertion forms, with and without the outer lookahead.
A UTF-8-flagged ssx|sy pattern must match sharp-s followed by x under /iu. Extracting the first s as a prefix splits the fold, so neither node can match the input character. This regressed in 9c721a4. Record the boundaries inside multi-character folds while building the words. Only extract a prefix at a boundary which is safe for every alternative, including overlapping folds in sss.
A folded word can lower the minimum length and raise the maximum at the same time. The else-if chain only updated the minimum in that case. Update both bounds independently. For eeeeee|a\xB5\xDFSss under /iu, the old maximum leaves the AC position ring too small for the second word. We can miss the match or fail an assertion.
Under /iu, s|zoo must not match sharp-s by consuming only the first s of its fold. When finding the end of a trie word, reject it if part of the input character's fold is left over. Try the next word in alternative order before running a continuation. A longer word may consume the whole fold and match.
Every native byte needs at most two UTF octets. Store the first in the low byte of a U16 and the second in the high byte. A zero high byte means there is no second octet, as continuation octets cannot be zero. This works for UTF-EBCDIC as well as UTF-8. We can fetch the encoding with one load, and the table shrinks from 768 to 512 bytes. We use the platform encoder to build it, without assuming ASCII byte values.
For nonfolded native input, consume both encoded octets in the same iteration. We need not return to the general reader between them. Keep both ownership checks, but only check for accepting words after the whole character. A word cannot end inside its encoding. Each native character is one input byte, so derive its position from the input pointer instead of counting characters in the loop. Tracing uses this same loop and shows the intermediate state on the character's diagnostic line. Together with packing, this saves about 8-13% of cycles on synthetic forty-character high-byte native match and miss cases, compared with the previous reader. These are nonthreaded GCC -O2 builds on an i7-1185G7. Some short folded and wide UTF-8 controls cost 1-2% more.
Advance and check the bounds after each successful word comparison. This gives GCC a smaller loop with shorter branches when scanning long runs of identical words.
Compare several machine words at a time so each block ends at the same position in the repeating sequence. This lets us skip runs such as three-octet Unicode spaces that a single-word comparison cannot handle. Try these periods when the ordinary word comparison fails. Check the run before building its comparison block, and keep the larger fallback out of the ordinary loop. On 32-bit builds, check the seed octets beyond the first word before skipping any possible starts.
Until today I didn't properly appreciate that internally our codepoints are not true Unicode codepoints, but actually hybrid codepoints where 0-255 map to the native encoding, and codepoints 256 and above follow Unicode. In ASCII environments the native codepoints and Unicode codepoints are identical, but in EBCDIC they aren't. So, "A" does not have codepoint 65 on EBCDIC. However this doesn't matter because most of the functions you might use on "A" which cares what codepoint is has will do the right thing in EBCDIC with the value it does have. Anyway, this patch updates some of the docs to use the term native-Unicode for the unicode apis - and provides a definition of the term so someone just reading a macro definition doesn't get confused. It also explains the subtlties here a bit more clearly. I kind of think that a function which takes a native-Unicode codepoint should call it ncp not cp as well. It does not change any code, just renames a var and update the comments to help others avoid the confustion I experienced here.
6ab6a01 to
e16d969
Compare
|
Id like to get this applied, it has multiple good things in it, and I think does no new harm. Also I have a bunch of work predicated on it. |
Skip the multi-character-fold closure scan when no folded trie start can begin such a fold. Generate a classifier from the first code point of each multi-character Case_Folding mapping, and track whether any trie start needs the scan. Keep the existing closure calculation when one does. The classifier consumes an already folded UTF string. It identifies the first character of a multi-character fold, rather than a character whose own fold expands to multiple characters. In a local comparison on CPU 2 using GCC 13.3, -O2, no debugging and no threads, the four-alternative fixture without a relevant fold start fell from a median 10.821 to 7.354 microseconds per compilation. The run used 24 alternating-order sample pairs targeting 0.4 seconds per baseline sample. These synthetic measurements include compilation and destruction, with setup excluded.
e16d969 to
dc67cf0
Compare
I've got no new comments at present and don't want to hold you up at this stage in the dev cycle. Only lingering concern is: how likely do you expect repeated UV blocks to feature in real-world workloads? Is it going to be enough for that part of the scanner to kick-in? |
Anecdotally I think scanning documents with lots of whitespace is pretty common. But its a good question. |
Resolve #24934 and
#24935.
Implements a grab bag of optimizations to the AHO-CORASICK start class optimization.
Uses Boyer-Moore-Horspool matching to construct a shift table which can allow scanning for the start of a match at very high speeds. (Think Boyer-Moore for a list of words.) However, BMH can slow things down in certain pathological cases, which is where we use a start class filter (similar to the old bitmap) to filter out bad starts, this combines with some logic to detect repeated short strings and consume them very quickly. The end result is that for the cases where the old bitmap was very fast we aren't quite as fast, but for the places where the bitmap was no help at all we can sometimes scream. Overall we seem to be very consistently fast, and sometimes very fast.
At the same time I added support for a start-octet map which uses the fold-inversion logic to build a bitmap we can use with utf8 folded data, which means case insensitive matches are now faster as well. Not as fast as case sensitive ones, alas.
There were also some other bookkeeping refinements which improved trie/ac matching, so even the base case got faster as well.
Lastly I found a bug that was introduced by the more aggressive prefix extraction that the octet patch included. An edge case that we hadnt encountered before, it is now fixed.
Median elapsed seconds; lower is faster. All builds use matching GCC 13.3
-O2settings, without debugging or threads./walrus|cat/, 200MAs +cat, Latin-1/walrus|cat/, random ASCII, Latin-1/abcdefgh|ijklmnop/, random ASCII, Latin-1/fooA|barB/, repeatedAB, Latin-1Medians of three trials. The previous-patch column comes from the preceding benchmark run; the other three columns are fresh measurements.
Workload sizes:
Acase: three scans.AB: 1 MiB, scanned 128 times.