Repository navigation
Use the regex cache for static Regex.Count and Regex.EnumerateSplits - #705
Merged
Merged
Conversation
This was referenced Oct 12, 2026
This was referenced Oct 13, 2026
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.
The static
Regex.Count(...)polyfills (six overloads, active below net7) and the staticRegex.EnumerateSplits(...)polyfills (three overloads, active below net9) constructed anew Regex(...)on every call. That re-parses the pattern each time and, withRegexOptions.Compiled, re-emits the IL each time. The BCL's own static methods go through its regex cache; the polyfill already carries a port of that cache (RegexCache) for the staticEnumerateMatchesoverloads.These overloads now go through
RegexCachetoo:RegexCache's guard is widened from!NET7_0_OR_GREATER && FeatureMemoryto!NET9_0_OR_GREATER, sinceCount(string, ...)does not needSystem.MemoryandEnumerateSplitsis polyfilled on net7 and net8. (This is why the generated API count for net7.0 and net8.0 goes up by one: the internal cache type is now included there.)RegexOptions.CultureInvariantis set. A regex bakes in the casing rules of the culture it was constructed under, so without this a regex cached under one culture would be handed to a caller running under another. That also fixes the same latent problem for the existing cachedEnumerateMatchesoverloads.GetOrAdd(pattern, options)overload constructsnew Regex(pattern, options), exactly as the old code did, so the default match timeout (REGEX_DEFAULT_MATCH_TIMEOUT) is still honoured. It is keyed separately from regexes constructed with an explicit timeout.GetOrAddthrowsArgumentNullExceptionfor a null pattern, asnew Regex(null)did. Patterns that fail to parse still throw on every call (nothing is cached for them).Measurements
Release, x64, best of 3.
net48:
Regex.Count(input, @"\b\w+\b")x 20,000Regex.Count(input, @"\b\w+\b", RegexOptions.IgnoreCase)x 20,000Regex.EnumerateSplits("a,b,c,d", ",")x 20,000Regex.Count(input, @"\b\w+\b", RegexOptions.Compiled)x 2,000Regex.EnumerateSplits("a,b,c,d", ",", RegexOptions.Compiled)x 2,000net8.0 (only
EnumerateSplitsis polyfilled there):Regex.EnumerateSplits("a,b,c,d", ",")x 20,000Regex.EnumerateSplits("a,b,c,d", ",", RegexOptions.Compiled)x 2,000The non-compiled gain on .NET Framework is modest because its
Regexconstructor has an internal cache of parsed patterns; the constructor on .NET Core / .NET 5+ does not, so the gain is larger there.Tests
Added to
PolyfillTests_Regex.cs:RegexCountStatic_RepeatedCalls,RegexCountSpanStatic_RepeatedCalls,EnumerateSplits_StaticMethod_RepeatedCalls: each call repeated so that later iterations are cache hitsRegexCountStatic_SamePatternDifferentOptions: one pattern with different options and timeouts must not share a cached regex (IgnoreCase,Multiline,IgnorePatternWhitespace,CultureInvariant, two timeouts)RegexCountStatic_ManyPatterns: 40 distinct patterns, three passes, so entries are evicted and rebuiltRegexCountStatic_Concurrent: 2,000 parallel calls over 60 patternsRegexCountStatic_UsesCurrentCulture: the sameIgnoreCasecall under en-US then tr-TR, each compared to a regex freshly constructed under that culture, plusCultureInvariantRegexCountStatic_InvalidArguments,EnumerateSplits_StaticMethod_InvalidPattern: unparseable patterns throw on every call; null pattern, null input, invalid timeout and invalid options keep their exception types