Skip to content

Commit 9242a9d

Browse files
committed
Reduce lock contention.
1 parent 3709747 commit 9242a9d

File tree

1 file changed

+48
-44
lines changed

1 file changed

+48
-44
lines changed

src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using JetBrains.Annotations;
22
using System;
3-
using System.Collections.Generic;
43
using System.ComponentModel;
54
using System.Threading;
65
using System.Threading.Tasks;
@@ -39,8 +38,10 @@ public T ExecuteUntilComplete<T>(ValueTask<T> valueTask)
3938
internal sealed class BenchmarkDotNetSynchronizationContext : SynchronizationContext
4039
{
4140
private readonly SynchronizationContext? previousContext;
42-
private readonly Queue<(SendOrPostCallback d, object? state)> queue = new();
43-
private bool isDisposed;
41+
// Use 2 arrays to reduce lock contention while executing. The common case is only 1 callback will be queued at a time.
42+
private (SendOrPostCallback d, object? state)[] queue = new (SendOrPostCallback d, object? state)[1];
43+
private (SendOrPostCallback d, object? state)[] executing = new (SendOrPostCallback d, object? state)[1];
44+
private int queueCount = 0;
4445
volatile private bool isCompleted;
4546

4647
internal BenchmarkDotNetSynchronizationContext(SynchronizationContext? previousContext)
@@ -59,28 +60,38 @@ public override void Post(SendOrPostCallback d, object? state)
5960
{
6061
ThrowIfDisposed();
6162

62-
queue.Enqueue((d, state));
63+
int index = queueCount;
64+
if (++queueCount > queue.Length)
65+
{
66+
Array.Resize(ref queue, queue.Length * 2);
67+
}
68+
queue[index] = (d, state);
69+
6370
Monitor.Pulse(queue);
6471
}
6572
}
6673

67-
private void ThrowIfDisposed()
68-
{
69-
if (isDisposed) throw new ObjectDisposedException(nameof(BenchmarkDotNetSynchronizationContext));
70-
}
74+
private void ThrowIfDisposed() => _ = queue ?? throw new ObjectDisposedException(nameof(BenchmarkDotNetSynchronizationContext));
7175

7276
internal void Dispose()
7377
{
78+
int count;
79+
(SendOrPostCallback d, object? state)[] executing;
7480
lock (queue)
7581
{
7682
ThrowIfDisposed();
77-
isDisposed = true;
7883

7984
// Flush any remaining posted callbacks.
80-
while (TryDequeue(out var callbackAndState))
81-
{
82-
callbackAndState.d(callbackAndState.state);
83-
}
85+
count = queueCount;
86+
queueCount = 0;
87+
executing = queue;
88+
queue = null;
89+
}
90+
this.executing = null;
91+
for (int i = 0; i < count; ++i)
92+
{
93+
executing[i].d(executing[i].state);
94+
executing[i] = default;
8495
}
8596
SetSynchronizationContext(previousContext);
8697
}
@@ -132,53 +143,46 @@ private void ExecuteUntilComplete()
132143
var spinner = new SpinWait();
133144
while (true)
134145
{
135-
if (TryDequeue(out var callbackAndState))
146+
int count;
147+
(SendOrPostCallback d, object? state)[] executing;
148+
lock (queue)
149+
{
150+
count = queueCount;
151+
queueCount = 0;
152+
executing = queue;
153+
queue = this.executing;
154+
}
155+
this.executing = executing;
156+
for (int i = 0; i < count; ++i)
157+
{
158+
executing[i].d(executing[i].state);
159+
executing[i] = default;
160+
}
161+
if (count > 0)
136162
{
137-
do
138-
{
139-
callbackAndState.d(callbackAndState.state);
140-
}
141-
while (TryDequeue(out callbackAndState));
142163
// Reset spinner after any posted callback is executed.
143164
spinner = new();
165+
continue;
144166
}
145167

146168
if (isCompleted)
147169
{
148170
return;
149171
}
150172

151-
if (spinner.NextSpinWillYield)
173+
if (!spinner.NextSpinWillYield)
152174
{
153-
// Yield the thread and wait for completion or for a posted callback.
154-
lock (queue)
155-
{
156-
Monitor.Wait(queue);
157-
}
158-
// Reset the spinner.
159-
spinner = new();
175+
spinner.SpinOnce();
160176
continue;
161177
}
162178

163-
spinner.SpinOnce();
164-
}
165-
}
166-
167-
private bool TryDequeue(out (SendOrPostCallback d, object? state) callbackAndState)
168-
{
169-
lock (queue)
170-
{
171-
#if NETSTANDARD2_0
172-
if (queue.Count > 0)
179+
// Yield the thread and wait for completion or for a posted callback.
180+
lock (queue)
173181
{
174-
callbackAndState = queue.Dequeue();
175-
return true;
182+
Monitor.Wait(queue);
176183
}
177-
callbackAndState = default;
178-
return false;
179-
#else
180-
return queue.TryDequeue(out callbackAndState);
181-
#endif
184+
// Reset the spinner.
185+
spinner = new();
182186
}
183187
}
184188
}

0 commit comments

Comments
 (0)