regex engine - compile tries using utf8 octets - #24712
Conversation
For now anyway. Maybe in the future we should just switch to double compiling, or maybe compiling on demand, and generate both a utf8 octet trie and a latin-1 version as well.
The trie compiler now builds trie transitions from the octets of the UTF-8 representation of the pattern. This gives every trie the same 256-octet alphabet and removes the need to represent Unicode codepoints as entries in a potentially very large transition alphabet. There is a special case for patterns whose codepoints are all ASCII: the ASCII and UTF-8 representations use the same octets, so they naturally share the same trie representation. Other low-page and native-string cases are handled as encoding details around this common UTF-8-byte representation, rather than as different trie formats. Encoding-specific matching rules, including native versus UTF-8 behavior and case folding, are handled independently of the fact that trie transitions are stored as UTF-8 octets. This keeps the representation uniform while preserving the existing matching semantics. Trie construction now uses the list representation throughout and converts it directly to the compressed transition form used by the executor. The old flat table construction path, character maps, reverse character map, wide character map, and inline-charclass trie op variants are removed. This eliminates a substantial amount of special handling for Unicode transitions and makes the implementation easier to maintain and prove correct. Trie indexes and related counters are widened to U32 where required, while the debug bitmap is kept as U8 data. Prefix extraction tracks encoded-byte and source-codepoint lengths so that UTF-8 prefixes are not split in the middle of a codepoint. Debug trie dumps now describe the octet alphabet and omit columns that are not present in the pattern. The regular expression internals version now identifies the octet-trie implementation, and the perldelta and regex internals documentation describe the new representation. Tests cover native and UTF-8 byte tries, updated diagnostics, and benchmark the scaling of large alternations with different amounts of padding. On this system, the normal seven-case corpus was measured with dumbbench using an untouched default configuration in both blead and this branch. The measured compile and match phase means were 0.109703 and 0.027088 seconds for blead, versus 0.103755 and 0.026573 seconds for the branch. This is a change of -5.4% and -1.9%, respectively. The branch was also measured with its trie disabled. The branch trie versus off change was +25.2% for compilation and -17.9% for matching. These numbers describe this small normal corpus; the large-alternation workload is where the asymptotic difference is most clear. On the branch-only large-alternation corpus, with 8 alternation counts from 10 through 1280 and 4 padding lengths from 10 through 80, the off model was dominated by the interaction term (n_alt * n_pad), with R^2 = 1.000 and an interaction t-statistic of 131.14. The trie model was dominated by const, with R^2 = 0.502 and an interaction t-statistic of -0.12. At 1280 alternations and 80 padding characters, off took 1.149000 seconds per child and trie took 0.012024 seconds, a 95.56x speedup. The benchmark uses dumbbench and optionally Statistics::Regression. When these are not available, it reports the corresponding cpanm command needed to install them.
khwilliamson
left a comment
There was a problem hiding this comment.
Here's the EXACTish node I created to work around trie constraints EXACTFAA_NO_TRIE - Like EXACTFAA, (string not UTF-8, folded except: MICRO,
SHARP S; folded length <= unfolded, not currently trie-able) */
I was wrong earlier about the existence of TRIEC in comments. Sorry for the noise.
I didn't trace through the code looking for unhandled edge cases. That would have taken a lot of time.
| else if (REGNODE_TYPE(OP(first)) == TRIE && | ||
| ((reg_trie_data *)RExC_rxi->data->data[ TRIE_DATA_SLOT(first) ])->minlen > 0) | ||
| ((reg_trie_data *)RExC_rxi->data->data[ TRIE_DATA_SLOT(first) ])->minlen > 0 | ||
| && ((reg_trie_data *)RExC_rxi->data->data[ TRIE_DATA_SLOT(first) ])->byte_trie != 2) |
There was a problem hiding this comment.
As a reader, I would prefer a mnemonic, like an enum, for the possible byte_trie values; I kept having to remember what means what.
| { | ||
| const U8 *end; | ||
|
|
||
| return is_utf8_string_loclen(uc, (STRLEN)(e - uc), &end, NULL) |
There was a problem hiding this comment.
Why not plain is_utf8_string(). It returns false unless the whole length is valid. Both function versions return true on plain ASCII
| } | ||
|
|
||
| /* EXACT_REQ8 and the UTF-8 folding nodes contain UTF-8 source bytes even | ||
| * when the pattern itself is not marked UTF-8. Treat those nodes as UTF-8 |
There was a problem hiding this comment.
I don't think this is true. I looked around, and couldn't find a case where one of these types of nodes could occur in a non-UTF8 pattern
| * here; otherwise a wide character is read one byte at a time and then | ||
| * encoded a second time when the byte trie is built. */ | ||
| #define TRIE_SOURCE_UTF8(noper) \ | ||
| (UTF || OP(noper) == EXACT_REQ8 || OP(noper) == EXACTFU_REQ8 \ |
There was a problem hiding this comment.
This made me consider if tries could apply to /l matching. I didn't look to see how that is handled; I presume that tries aren't generated because of the complications involved. But I suppose they could be if the pattern didn't have folding and all branches didn't have anything like [[:alpha:]] in them. But it's probably not worth the effort
There was a problem hiding this comment.
Historically we couldn't trie locale specific patterns because the meaning of the pattern can change depending on the current locale. So the trie table would have to be remapped.
| */ | ||
|
|
||
|
|
||
| /* UTF-8 byte tries can have up to UTF8_MAXBYTES transitions for each |
There was a problem hiding this comment.
If we restricted tries to running only on Unicode characters, we could save space with likely no one being affected. The symbol for that is MAX_UNICODE_UTF8_BYTES
There was a problem hiding this comment.
Why would we need to restrict tries to running only on unicode characters? What we do is we build a trie contining utf8 representations of the data, and then translate the awkward codepoints on the fly. so from the tries point of view all data is utf8.
| U8 *ep; | ||
| U8 *bp; | ||
| if (!TRIE_SOURCE_UTF8(noper)) | ||
| uvc = NATIVE_TO_UNI(uvc); |
There was a problem hiding this comment.
I think this breaks on EBCDIC. There may be other similar places as well. This changes uvc for code outside this block. And it's trying to construct a trie based on not the native values, but the official Unicode ones. I don't understand why. Mostly EBCDIC just works these days. One codes without thinking about it except in rare cases. The native value is what one wants
There was a problem hiding this comment.
I dont remember this part, ill review it and get back to you.
| * initial unused element. Preserve the theoretical packed size | ||
| * before using transcount as the growable allocation size. */ | ||
| const STRLEN transition_count = transcount - 1; | ||
| PERL_UNUSED_VAR(transition_count); |
There was a problem hiding this comment.
I don't understand why this is needed
| else | ||
| break; | ||
| } | ||
| else if (utf8_prefix[bytes] < 0x80 |
There was a problem hiding this comment.
The mnemonic ! UTF8_IS_CONTINUATION should be used.
| break; | ||
|
|
||
| if (!need) { | ||
| if (utf8_prefix[bytes] < 0x80) |
There was a problem hiding this comment.
Maybe this is why you translated native to unicode. But again EBCDIC generally just works as long as you use the mnemonic macro (which is clearer to the reader anyway).
Why not use UTF8SKIP() - 1 instead of this series of conditionals? I think that would mean this would automatically extend to above-Unicode code points, without the break currently done
There was a problem hiding this comment.
The new code depends on being able to efficiently convert non-utf8 to utf8 to handle the awkward codepoints. that is the whole point here, the data it always stored in utf8. always. (it could be stored as EBCDIC utf8 or whatever if there is such a thing.). Where can we test such a platform?
The trie compiler now builds trie transitions from the octets of the
UTF-8 representation of the pattern. This gives every trie the same
256-octet alphabet and removes the need to represent Unicode codepoints
as entries in a potentially very large transition alphabet.
There is a special case for patterns whose codepoints are all ASCII: the
ASCII and UTF-8 representations use the same octets, so they naturally
share the same trie representation. Other low-page and native-string
cases are handled as encoding details around this common UTF-8-byte
representation, rather than as different trie formats.
Encoding-specific matching rules, including native versus UTF-8 behavior
and case folding, are handled independently of the fact that trie
transitions are stored as UTF-8 octets. This keeps the representation
uniform while preserving the existing matching semantics.
Trie construction now uses the list representation throughout and
converts it directly to the compressed transition form used by the
executor. The old flat table construction path, character maps, reverse
character map, wide character map, and inline-charclass trie op variants
are removed. This eliminates a substantial amount of special handling
for Unicode transitions and makes the implementation easier to maintain
and prove correct.
Trie indexes and related counters are widened to U32 where required,
while the debug bitmap is kept as U8 data. Prefix extraction tracks
encoded-byte and source-codepoint lengths so that UTF-8 prefixes are not
split in the middle of a codepoint. Debug trie dumps now describe the
octet alphabet and omit columns that are not present in the pattern.
The regular expression internals version now identifies the octet-trie
implementation, and the perldelta and regex internals documentation
describe the new representation. Tests cover native and UTF-8 byte
tries, updated diagnostics, and benchmark the scaling of large
alternations with different amounts of padding.