Repository navigation
Skip to the bytes a pattern's first item can match before entering the matcher - #56
Merged
Merged
Conversation
…e matcher find, match, gsub and gmatch tried the matcher at every position of the subject. A pattern whose first item is a literal, a class or a set, and must take a byte, can only match at a byte that item accepts; most bytes of a text are not one. The set of such bytes is built once per pattern, cached in the executor, and a scan skips to the next candidate with a byte loop, or IndexByte for a set of one byte. gsub copies the bytes it skips in one piece. Skipped bytes and the build are charged to the step budget, so Limits.Steps bounds a scan as before. The scan of a set for its ']' moves into setEnd, which neither charges nor raises, so that the build has no side effects; classEnd does both as before. Class and set scans over a megabyte go from 8 to 15 ms to under 1 ms, several times faster than C Lua 5.4; dense matches and anchored patterns are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
string.find,match,gsubandgmatchtry the matcher at every position of the subject. For most patterns that is wasted work: a pattern whose first item is a literal, a class (%d) or a set ([\128-\255]) can only match at a byte that item accepts, and most bytes of a text are not one. This change computes that set once per pattern and skips to the next candidate byte with a plain loop, orstrings.IndexBytewhen the set is one byte.gsubcopies the bytes it skips to its builder in one piece instead of one at a time.The set is built only when the first item must take a byte: not after
^, not for., not for an item followed by*,?or-, not for$alone, a position capture,%b,%for a back-reference, and not when the matcher would fail before the item (33 captures open). A capture that opens on such an item ((%d+)) qualifies. The sets are cached by pattern in the executor, 64 at a time and cleared between entries, as building one costs 256 tests, more than a short subject's scan; patterns over 256 bytes are built each time and not kept, and a set item over 64 bytes is left to the matcher.The step budget stays honest: the build is charged 256 times the item's length, and the bytes skipped are charged whether or not a candidate is found, so
Limits.Stepsbounds a scan as before. To keep the build free of side effects, the scan of a[set]for its]moves fromclassEndintosetEnd, which neither charges nor raises;classEnddoes both as before.Motivation
Text processing scripts spend much of their time in
find,gsubandgmatchwith patterns that match sparsely: stripping control characters, finding the first byte over 127, splitting on a delimiter, replacing a literal. Profiling such scripts against a C Lua 5.4 build showed every pattern scan running 1.7 to 1.8 times slower than C Lua, while the matcher's cost per call was already at parity. The gap was entirely the per-position loop around the matcher. With this change those scans run several times faster than C Lua, and a script that parses and rewrites a 1 MiB message bundle with a mix of these calls goes from 62 ms to 45 ms (C Lua: 47).Over 1 MiB of text, arm64:
find("[\128-\255]"), no matchgsub("[%z\1-\8\11-\31\127]", ""), no matchgsub("[\128-\255]", fn), a quarter of the bytes matchgsub("[\128-\255]", table)gsub("\n", "")gmatch("%S+"), dense wordsmatch("^(%d+)/(%d+)")per short wordAnchored patterns and dense matches are unchanged, as expected: the matcher's per-call cost is already at parity with C Lua.
Testing
TestPatternFirstSetcovers the first items that take a byte, those that may take none, anchors, captures, position captures, balances, frontiers, back-references, the gsub limit, gmatch's rules for a literal^and for empty matches, malformed patterns still raising, "too many captures" still raising when no candidate byte exists, the cache past 64 patterns, a pattern too long to cache, the step charge for skipped bytes (found or not) and for the build, and a gmatch iterator that the budget stopped mid-scan being called again by a closer.TestDifferentialwithGLUA_DIFF_N=3000against C Lua 5.4.7: no pattern differences. On macOS the test also reportsos.timewith negative years and%gof nan, libc differences that predate this change; CI runs it on Linux.go vet,go test,-race,-d=checkptr,-tags lua_stackstress,bench/asmgateandgenloop -checkall pass.