Skip to content

Use the regex cache for static Regex.Count and Regex.EnumerateSplits - #705

Merged
SimonCropp merged 1 commit into
mainfrom
perf/regex-static-cache
Oct 12, 2026
Merged

SimonCropp merged 1 commit into
mainfrom
perf/regex-static-cache

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

The static Regex.Count(...) polyfills (six overloads, active below net7) and the static Regex.EnumerateSplits(...) polyfills (three overloads, active below net9) constructed a new Regex(...) on every call. That re-parses the pattern each time and, with RegexOptions.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 static EnumerateMatches overloads.

These overloads now go through RegexCache too:

  • RegexCache's guard is widened from !NET7_0_OR_GREATER && FeatureMemory to !NET9_0_OR_GREATER, since Count(string, ...) does not need System.Memory and EnumerateSplits is 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.)
  • The cache key gains the culture, as in the BCL: the current culture name, or empty when RegexOptions.CultureInvariant is 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 cached EnumerateMatches overloads.
  • A GetOrAdd(pattern, options) overload constructs new 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.
  • GetOrAdd throws ArgumentNullException for a null pattern, as new 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:

call before after
Regex.Count(input, @"\b\w+\b") x 20,000 60.44 ms 28.77 ms
Regex.Count(input, @"\b\w+\b", RegexOptions.IgnoreCase) x 20,000 65.37 ms 30.75 ms
Regex.EnumerateSplits("a,b,c,d", ",") x 20,000 34.99 ms 6.61 ms
Regex.Count(input, @"\b\w+\b", RegexOptions.Compiled) x 2,000 7,243 ms 7.81 ms
Regex.EnumerateSplits("a,b,c,d", ",", RegexOptions.Compiled) x 2,000 3,717 ms 1.45 ms

net8.0 (only EnumerateSplits is polyfilled there):

call before after
Regex.EnumerateSplits("a,b,c,d", ",") x 20,000 18.46 ms 5.69 ms
Regex.EnumerateSplits("a,b,c,d", ",", RegexOptions.Compiled) x 2,000 1,822 ms 2.21 ms

The non-compiled gain on .NET Framework is modest because its Regex constructor 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 hits
  • RegexCountStatic_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 rebuilt
  • RegexCountStatic_Concurrent: 2,000 parallel calls over 60 patterns
  • RegexCountStatic_UsesCurrentCulture: the same IgnoreCase call under en-US then tr-TR, each compared to a regex freshly constructed under that culture, plus CultureInvariant
  • RegexCountStatic_InvalidArguments, EnumerateSplits_StaticMethod_InvalidPattern: unparseable patterns throw on every call; null pattern, null input, invalid timeout and invalid options keep their exception types

@SimonCropp SimonCropp modified the milestones: 11.4.4, 11.4.5 Oct 12, 2026
@SimonCropp
SimonCropp merged commit 25cf064 into main Oct 12, 2026
5 of 7 checks passed
@SimonCropp
SimonCropp deleted the perf/regex-static-cache branch October 12, 2026 12:10
This was referenced Oct 12, 2026
This was referenced Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant