Repository navigation
Conversation
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @dotnet/area-system-text-regularexpressions |
cshung
force-pushed
the
cshung/optimize-regex-allocation
branch
2 times, most recently
from
October 28, 2026 18:51
e627549 to
ba92ff0
Compare
This was referenced Oct 28, 2026
Store NonBacktracking symbolic nodes in builder-owned arrays and reference them with compact builder-relative IDs. This reduces retained object graph size and recurring GC traversal costs while preserving hot matching-state metadata. Signed-off-by: Andrew Au <3410332+cshung@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Satisfy CA1066 and route object equality through the typed ID-based implementation. Signed-off-by: Andrew Au <3410332+cshung@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Andrew Au <3410332+cshung@users.noreply.github.com>
cshung
force-pushed
the
cshung/optimize-regex-allocation
branch
from
October 30, 2026 15:33
ba92ff0 to
8edb6fb
Compare
Contributor
Author
|
@stephentoub, please take a look? |
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Regression coverage does not exercise subtree traversal through a retained parent context after resizing or concurrent node publication across capacity boundaries.
1 open issue
What changed in this PR
Reduces GC overhead in the NonBacktracking regex engine by replacing individually allocated symbolic nodes with compact IDs into a builder-owned arena, while aiming to preserve matching behavior.
Changes:
- Adds arena storage, immutable node records, and snapshot-based traversal.
- Converts caches and matching states to node IDs, with precomputed state metadata.
- Updates symbolic-engine callers and adds handle-size, interning, and basic arena-growth tests.
| File | Description |
|---|---|
src/libraries/System.Text.RegularExpressions/tests/UnitTests/SymbolicRegexTests.cs |
Adds handle and arena tests; updates builder-aware calls. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexRunnerFactory.cs |
Passes the builder to size estimation. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexNode.cs |
Introduces ID handles, snapshot contexts, and iterative singleton counting. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexMatcher.Explore.cs |
Resolves state node IDs during exploration. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexMatcher.Dgml.cs |
Uses builder-aware node access for graph output. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexMatcher.cs |
Updates traversal and caches pattern anchor metadata. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexMatcher.Automata.cs |
Keys states by node ID and updates transition construction. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexKind.cs |
Updates comments to describe arena node data. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexBuilder.cs |
Owns arena allocation, publication, and ID-based caches. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/RegexNodeConverter.cs |
Supplies the builder for node predicates. |
src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/MatchingState.cs |
Stores node IDs and precomputes matching metadata. |
Review effort: Balanced
Cover retained snapshot traversal after arena growth and concurrent publication across multiple capacity boundaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Andrew Au <3410332+cshung@users.noreply.github.com>
This branch has not been deployed
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.

Summary
The NonBacktracking regex engine currently represents its symbolic regex as a DAG of individually allocated
SymbolicRegexNode<TSet>objects. Complex expressions can generate more than 100,000 retained nodes while lazily constructing DFA/NFA states. These objects and their child references must be traversed during every full GC.This change stores symbolic nodes as immutable records in an array owned by
SymbolicRegexBuilder<TSet>. References between nodes are represented by four-byte, builder-relative integer IDs.For node-heavy expressions, this reduces retained memory by approximately 16% and forced full-GC time by 75–91%. Warmed matching performance on representative Email, URL, and IPv4 expressions remains within approximately ±2%.
Design
Previously, each symbolic node was a separate object containing references to its child nodes:
The new representation is:
SymbolicRegexNode<TSet>is now a four-byte value-type handle. ID 0 represents the null/default handle; positive IDs index immutableNodeDatarecords in the owning builder.Node IDs are builder-relative. Operations that interpret an ID therefore also receive the owning builder. Temporary
SymbolicRegexNodeContext<TSet>values carry:Child traversal reuses the same snapshot rather than reacquiring the arena for each field.
MatchingState<TSet>remains a reference type, but stores a node ID instead of a node reference. It precomputes nullability, dead-end, anchor, and state-flag information so established warmed matching loops do not repeatedly access the arena.Node interning and recursive-operation caches now store node IDs. Structurally identical expressions therefore continue to share the same node, and the symbolic representation remains a maximally shared DAG.
Concurrent publication
Nodes are immutable once published and are appended under the matcher's existing synchronization.
Publication follows this order:
NodeData.Readers acquire-read the node count before capturing the arena array.
Children are always published before their parent. Consequently, any array snapshot containing a parent also contains its complete subtree. A context may continue using an older array after the arena grows because published records are immutable and old arrays remain valid snapshots.
A regular
List<NodeData>cannot provide these semantics:List<T>.Addupdates its count before storing the new item, does not release-publish either its count or backing array, and does not expose a stable backing-array snapshot.Node-heavy results
The stress cases construct approximately 125,000 symbolic nodes by matching against a 50,000-character input.
Patterns:
Each result below is the median of three independent processes. The regex and input were retained while 15 forced full GCs were measured per process.
Memory
Forced full-GC median
The reduction in GC time is larger than the reduction in retained bytes because the old representation required marking many separate node objects and traversing their reference edges. The new representation concentrates the node data in a small number of arrays with integer child links.
Representative regex benchmarks
BenchmarkDotNet results using NonBacktracking Email, URL, and IPv4 expressions:
IsMatchNegative values indicate improvement. Allocations were effectively unchanged for these comparatively small expressions.
These results indicate that arena lookup does not introduce a warmed matching regression. Small expressions also do not receive a meaningful memory benefit because their symbolic graphs are not large enough for node objects to dominate.
Cold-path tradeoff
The pathological 125,000-node stress cases have a variable one-time construction/first-match cost. Across independent process measurements, results ranged from approximately neutral to a 30–37% regression, corresponding to roughly 100–200 ms.
CPU traces indicate that most of this cold cost is additional JIT compilation and generic code expansion in the symbolic-engine methods, rather than arena resizing or warmed node access:
This is runtime JIT compilation of the generic symbolic engine, not generated or compiled regex code. Repeated constructions in the same process do not continue paying the full cost, and warmed matching remains neutral.
An experiment reducing inlining lowered generated code size but did not improve end-to-end cold time, so it was not retained.
Scope
This affects only the symbolic representation used by the NonBacktracking engine. It does not change:
RegexNodetree.The implementation is managed code and contains no architecture-specific branches or intrinsics. Correctness is expected to be portable across operating systems and architectures, although the exact memory, JIT, and GC performance effects will vary.
Validation
System.Text.RegularExpressionsbuild.Note
This PR description was prepared with GitHub Copilot.