@@ -32,6 +32,7 @@ public partial class Regex : ISerializable
32
32
33
33
private WeakReference < RegexReplacement ? > ? _replref ; // cached parsed replacement pattern
34
34
private volatile RegexRunner ? _runner ; // cached runner
35
+ private RegexRunnerPool ? _runnerPool ; // pool of cached runners to spill into
35
36
36
37
#if DEBUG
37
38
// These members aren't used from Regex(), but we want to keep them in debug builds for now,
@@ -421,7 +422,7 @@ protected void InitializeReferences()
421
422
ThrowHelper . ThrowArgumentOutOfRangeException ( ExceptionArgument . length , ExceptionResource . LengthNotNegative ) ;
422
423
}
423
424
424
- RegexRunner runner = Interlocked . Exchange ( ref _runner , null ) ?? CreateRunner ( ) ;
425
+ RegexRunner runner = RentOrCreateRunner ( ) ;
425
426
try
426
427
{
427
428
runner . InitializeTimeout ( internalMatchTimeout ) ;
@@ -453,7 +454,7 @@ protected void InitializeReferences()
453
454
finally
454
455
{
455
456
runner . runtext = null ; // drop reference to text to avoid keeping it alive in a cache.
456
- _runner = runner ;
457
+ ReturnRunner ( runner ) ;
457
458
}
458
459
}
459
460
@@ -466,7 +467,7 @@ protected void InitializeReferences()
466
467
// that takes in startat.
467
468
Debug . Assert ( startat <= input . Length ) ;
468
469
469
- RegexRunner runner = Interlocked . Exchange ( ref _runner , null ) ?? CreateRunner ( ) ;
470
+ RegexRunner runner = RentOrCreateRunner ( ) ;
470
471
try
471
472
{
472
473
runner . InitializeTimeout ( internalMatchTimeout ) ;
@@ -513,7 +514,7 @@ protected void InitializeReferences()
513
514
}
514
515
finally
515
516
{
516
- _runner = runner ;
517
+ ReturnRunner ( runner ) ;
517
518
}
518
519
}
519
520
@@ -529,7 +530,7 @@ private void RunAllMatchesWithCallback<TState>(string? inputString, ReadOnlySpan
529
530
Debug . Assert ( inputString is null || inputSpan . SequenceEqual ( inputString ) ) ;
530
531
Debug . Assert ( ( uint ) startat <= ( uint ) inputSpan . Length ) ;
531
532
532
- RegexRunner runner = Interlocked . Exchange ( ref _runner , null ) ?? CreateRunner ( ) ;
533
+ RegexRunner runner = RentOrCreateRunner ( ) ;
533
534
try
534
535
{
535
536
runner . runtext = inputString ;
@@ -599,7 +600,7 @@ private void RunAllMatchesWithCallback<TState>(string? inputString, ReadOnlySpan
599
600
finally
600
601
{
601
602
runner . runtext = null ; // drop reference to string to avoid keeping it alive in a cache.
602
- _runner = runner ;
603
+ ReturnRunner ( runner ) ;
603
604
}
604
605
}
605
606
@@ -643,11 +644,43 @@ private void RunAllMatchesWithCallback<TState>(string? inputString, ReadOnlySpan
643
644
return RegularExpressions . Match . Empty ;
644
645
}
645
646
646
- /// <summary>Creates a new runner instance.</summary>
647
- private RegexRunner CreateRunner ( ) =>
648
- // The factory needs to be set by the ctor. `factory` is a protected field, so it's possible a derived
647
+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
648
+ private RegexRunner RentOrCreateRunner ( )
649
+ {
650
+ RegexRunner ? runner = Interlocked . Exchange ( ref _runner , null ) ;
651
+ if ( runner != null )
652
+ {
653
+ return runner ;
654
+ }
655
+
656
+ RegexRunnerPool ? pool = _runnerPool ;
657
+ if ( pool != null && pool . TryGet ( out runner ) )
658
+ {
659
+ return runner ;
660
+ }
661
+
662
+ // The factory needs to be set by the ctor. `factory` is a protected field, so it's possible a derived
649
663
// type nulls out the factory after we've set it, but that's the nature of the design.
650
- factory ! . CreateInstance ( ) ;
664
+ return factory ! . CreateInstance ( ) ;
665
+ }
666
+
667
+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
668
+ private void ReturnRunner ( RegexRunner runner )
669
+ {
670
+ if ( _runner is null )
671
+ {
672
+ // If we don't have a runner, then we can just store this one.
673
+ _runner = runner ;
674
+ }
675
+ else
676
+ {
677
+ // If we reached here, it means that another operation has won the race and already stored a runner.
678
+ // Use this condition to detect contended runner usage and initialize a pool to store the runner in.
679
+ // Here, we may also lose the race and create more than one pool. This is acceptable as the goal is to
680
+ // reduce the number of ammortized allocations at reasonable cost rather than eliminating every single one.
681
+ ( _runnerPool ??= new ( ) ) . Return ( runner ) ;
682
+ }
683
+ }
651
684
652
685
/// <summary>True if the <see cref="RegexOptions.Compiled"/> option was set.</summary>
653
686
[ Obsolete ( Obsoletions . RegexExtensibilityImplMessage , DiagnosticId = Obsoletions . RegexExtensibilityDiagId , UrlFormat = Obsoletions . SharedUrlFormat ) ]
0 commit comments