Repository navigation
[RFC] ext/strmatch: Add prepared string matching and replacement - #25122
Open
ArtUkrainskiy wants to merge 1 commit into
Open
ArtUkrainskiy wants to merge 1 commit into
ArtUkrainskiy wants to merge 1 commit into
Conversation
ArtUkrainskiy
force-pushed
the
str-matcher-prototype
branch
from
November 2, 2026 19:09
5025a32 to
f212775
Compare
ArtUkrainskiy
force-pushed
the
str-matcher-prototype
branch
from
November 2, 2026 19:48
f212775 to
287aaab
Compare
Introduce independent final Str\Matcher and Str\Replacer classes for reusing fixed keyword dictionaries across subjects. Build hybrid Aho-Corasick automata with dense and packed sparse transitions. Use byte-based matching and reversed matching for leftmost-longest replacement, while retaining shared keyword strings. Collect sparse replacement candidates and switch to a position-indexed buffer when their density warrants it. Check state, count and result limits, reject repeated initialization, and keep prepared objects uncloneable, non-serializable and incomparable. Include Unix and Windows build configuration, CI coverage and 16 PHPT tests for API semantics, binary input, overlapping matches and lifecycle.
ArtUkrainskiy
force-pushed
the
str-matcher-prototype
branch
from
November 5, 2026 10:41
287aaab to
7c7550b
Compare
ArtUkrainskiy
marked this pull request as ready for review
November 6, 2026 12:59
TimWolla
reviewed
Nov 6, 2026
TimWolla
left a comment
Member
There was a problem hiding this comment.
This absolutely should be part of ext/standard. Making it a separate extension is needless (maintenance) overhead.
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.
This PR contains the current implementation in ext/strmatch. The RFC proposes integration into ext/standard; the final placement and naming remain open for discussion.
RFC: https://wiki.php.net/rfc/prepared_literal_string_matching
Discussion: https://news-web.php.net/php.internals/133028
I propose
Str\MatcherandStr\Replacerfor preparing a fixed literal dictionary once and using it across multiple texts. Matcher supports presence checks, unique matched keys, counting and enumerating overlapping occurrences;Str\MatchResultexposes byte offsets, lengths and keys. Replacer selects the earliest match, preferring the longest key at that position, and does not rescan inserted text.Both classes capture an immutable dictionary and use Aho–Corasick with hybrid dense/sparse transitions. Matching is byte-oriented, binary-safe and case-sensitive. Reuse is within one request or a long-running process.
AMD Ryzen 9 5950X, Linux x86-64, GCC 15.2 / Clang 21.1. Times are medians; parentheses show median paired elapsed-time changes against the fastest eligible existing method in each measurement. Negative percentages mean faster; slowdowns of at least 2× are shown as ×N slower. Prepared includes the call and result release on an existing object; one-shot also includes construction and destruction. Existing methods use prebuilt arguments and warm PCRE caches. The displayed existing-method times are medians from the one-shot comparisons; prepared calls have separately measured references.
matches(); 128 B; 128 keys; no matchespreg_match()GCC: 3.048 µs
Clang: 3.038 µs
matches(); 4 KiB; 128 keys; no matchespreg_match()GCC: 100.021 µs
Clang: 100.014 µs
matches(); 4 KiB; 128 keys; hit at byte 8preg_match()GCC: 0.414 µs
Clang: 0.408 µs
matches(); 4 KiB; 128 keys; hit at byte 2042preg_match()GCC: 51.972 µs
Clang: 51.897 µs
matches(); 4 KiB; 128 keys; hit at byte 4076str_contains()loopGCC: 60.558 µs
Clang: 56.447 µs
matches(); 32 KiB; 1024 keys; no matchesstr_contains()loopGCC: 7699.530 µs
Clang: 7317.499 µs
getMatchedKeywords(); 4 KiB; 128 keys; 1 occurrencepreg_match_all()GCC: 102.852 µs
Clang: 102.811 µs
getMatchedKeywords(); natural EN text, 12828 B; 128 keys; 23 occurrences, 12 unique keysstrpos()loopGCC: 307.035 µs
Clang: 289.993 µs
countMatches(); 4 KiB; 128 keys; 1 occurrencepreg_match_all()GCC: 101.504 µs
Clang: 101.701 µs
matchAll(); 64 KiB; 128 keys; 1 occurrencepreg_match_all()GCC: 1610.465 µs
Clang: 1614.484 µs
matchAll(); 64 KiB; the same 128 keys; 64 occurrences, 4 unique keyspreg_match_all()GCC: 1695.893 µs
Clang: 1702.396 µs
replace(); 1 KiB; 32 keys; 2 replacementspreg_replace_callback()GCC: 2.422 µs
Clang: 2.566 µs
replace(); 4 KiB; the same 32 keys; 0 replacementspreg_replace_callback()GCC: 8.482 µs
Clang: 8.173 µs
replace(); 4 KiB; the same 32 keys; 16 replacementspreg_replace_callback()GCC: 9.380 µs
Clang: 9.221 µs
replace(); 16 KiB; the same 32 keys; 16 replacementspreg_replace_callback()GCC: 62.595 µs
Clang: 61.774 µs
replace(); 4 KiB; 1 key of 8 B; 5 replacementsstrtr()GCC: 0.400 µs
Clang: 0.390 µs
replace(); 1 MiB; 1000 keys of 8–24 B; 1000 replacementspreg_replace_callback()GCC: 13762.732 µs
Clang: 13679.984 µs
replace(); 1 MiB; 10000 keys of 16 B; 0 replacementsstrtr()GCC: 7045.148 µs
Clang: 6573.642 µs
replace(); the same 1 MiB text; 10000 keys of 8–24 B; 0 replacementsstrtr()GCC: 89600.329 µs
Clang: 89526.581 µs
replace(); 1 MiB; 1000 keys in{{…}}format; 100 replacementspreg_replace_callback()GCC: 63.977 µs
Clang: 64.276 µs
Each row uses one fixed text and dictionary. Controlled Matcher keys are 12 B; the natural-text dictionary uses keys of 4–28 B. In the three positive
matches()rows, the matching key is #65 in dictionary order. The 32-key Replacer rows share keys of 11–21 B and same-length replacement values.For
{{…}}keys, the specialized PCRE expression finds candidates and looks them up in the dictionary. These cases illustrate different trade-offs, not universal thresholds or production workload frequencies. † Observed time only: the measurement did not pass quality checks.The extension builds with
-Werroron GCC and Clang; 16 PHPT tests pass on both. Independent output checks pass under ASan/UBSan.Full results, memory costs, exact inputs, correctness logs and reproduction.