Skip to content

Commit e641312

Browse files
committed
Introduce RegexRunnerPool to reuse multiple RegexRunner instances at once (usually under contention)
1 parent a01d6d6 commit e641312

File tree

3 files changed

+84
-10
lines changed

3 files changed

+84
-10
lines changed

src/libraries/System.Text.RegularExpressions/src/System.Text.RegularExpressions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<Compile Include="System\Text\RegularExpressions\RegexRunner.cs" />
5353
<Compile Include="System\Text\RegularExpressions\RegexRunnerFactory.cs" />
5454
<Compile Include="System\Text\RegularExpressions\RegexRunnerMode.cs" />
55+
<Compile Include="System\Text\RegularExpressions\RegexRunnerPool.cs" />
5556
<Compile Include="System\Text\RegularExpressions\RegexTree.cs" />
5657
<Compile Include="System\Text\RegularExpressions\RegexTreeAnalyzer.cs" />
5758
<Compile Include="System\Text\RegularExpressions\RegexWriter.cs" />

src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Regex.cs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public partial class Regex : ISerializable
3232

3333
private WeakReference<RegexReplacement?>? _replref; // cached parsed replacement pattern
3434
private volatile RegexRunner? _runner; // cached runner
35+
private RegexRunnerPool? _runnerPool; // pool of cached runners to spill into
3536

3637
#if DEBUG
3738
// 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()
421422
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length, ExceptionResource.LengthNotNegative);
422423
}
423424

424-
RegexRunner runner = Interlocked.Exchange(ref _runner, null) ?? CreateRunner();
425+
RegexRunner runner = RentOrCreateRunner();
425426
try
426427
{
427428
runner.InitializeTimeout(internalMatchTimeout);
@@ -453,7 +454,7 @@ protected void InitializeReferences()
453454
finally
454455
{
455456
runner.runtext = null; // drop reference to text to avoid keeping it alive in a cache.
456-
_runner = runner;
457+
ReturnRunner(runner);
457458
}
458459
}
459460

@@ -466,7 +467,7 @@ protected void InitializeReferences()
466467
// that takes in startat.
467468
Debug.Assert(startat <= input.Length);
468469

469-
RegexRunner runner = Interlocked.Exchange(ref _runner, null) ?? CreateRunner();
470+
RegexRunner runner = RentOrCreateRunner();
470471
try
471472
{
472473
runner.InitializeTimeout(internalMatchTimeout);
@@ -513,7 +514,7 @@ protected void InitializeReferences()
513514
}
514515
finally
515516
{
516-
_runner = runner;
517+
ReturnRunner(runner);
517518
}
518519
}
519520

@@ -529,7 +530,7 @@ private void RunAllMatchesWithCallback<TState>(string? inputString, ReadOnlySpan
529530
Debug.Assert(inputString is null || inputSpan.SequenceEqual(inputString));
530531
Debug.Assert((uint)startat <= (uint)inputSpan.Length);
531532

532-
RegexRunner runner = Interlocked.Exchange(ref _runner, null) ?? CreateRunner();
533+
RegexRunner runner = RentOrCreateRunner();
533534
try
534535
{
535536
runner.runtext = inputString;
@@ -599,7 +600,7 @@ private void RunAllMatchesWithCallback<TState>(string? inputString, ReadOnlySpan
599600
finally
600601
{
601602
runner.runtext = null; // drop reference to string to avoid keeping it alive in a cache.
602-
_runner = runner;
603+
ReturnRunner(runner);
603604
}
604605
}
605606

@@ -643,11 +644,43 @@ private void RunAllMatchesWithCallback<TState>(string? inputString, ReadOnlySpan
643644
return RegularExpressions.Match.Empty;
644645
}
645646

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
649663
// 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+
}
651684

652685
/// <summary>True if the <see cref="RegexOptions.Compiled"/> option was set.</summary>
653686
[Obsolete(Obsoletions.RegexExtensibilityImplMessage, DiagnosticId = Obsoletions.RegexExtensibilityDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Concurrent;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Threading;
7+
8+
namespace System.Text.RegularExpressions
9+
{
10+
internal sealed class RegexRunnerPool
11+
{
12+
private static readonly int s_maxCapacity = Math.Max(4, Environment.ProcessorCount);
13+
14+
private ConcurrentStack<RegexRunner> _storage = new();
15+
private int _storedCount;
16+
17+
public bool TryGet([NotNullWhen(true)] out RegexRunner? runner)
18+
{
19+
if (_storage.TryPop(out runner))
20+
{
21+
Interlocked.Decrement(ref _storedCount);
22+
return true;
23+
}
24+
25+
return false;
26+
}
27+
28+
public void Return(RegexRunner runner)
29+
{
30+
if (Interlocked.Increment(ref _storedCount) <= s_maxCapacity)
31+
{
32+
_storage.Push(runner);
33+
}
34+
else
35+
{
36+
Interlocked.Decrement(ref _storedCount);
37+
}
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)