Fast ManualResetEventSlim checks when waiting is rarely needed
I was looking at some code optimizations for the RabbitMQ .NET client and noticed that in one place on the hot path the Wait() method was being called on a ManualResetEventSlim which was already set most of the time, and thought that might be overkill.
I was looking at some code optimizations for the RabbitMQ .NET client and noticed that in one place on the hot path the Wait()
method was being called on a ManualResetEventSlim which was already set most of the time, and thought that might be overkill.
I figured that calling Wait()
when it should just complete most of the time was most likely overkill, so I decided to check if things could be made faster. I thought of two options, first calling Wait(0)
which tries to wait with a timeout of zero and then also to simply check the IsSet
property first.
I wrote the following code using BenchmarkDotNet:
public class ManualResetEventSlimChecks
{
private ManualResetEventSlim _set = new ManualResetEventSlim(true);
[Benchmark]
public void CheckSetBit()
{
if (!_set.IsSet)
{
_set.Wait();
}
}
[Benchmark]
public void WaitZero()
{
if (!_set.Wait(0))
{
_set.Wait();
}
}
[Benchmark(Baseline = true)]
public void Wait()
{
_set.Wait();
}
}
This yielded the following results:
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.21313
Intel Core i7-10700 CPU 2.90GHz, 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=5.0.103
[Host] : .NET Core 3.1.12 (CoreCLR 4.700.21.6504, CoreFX 4.700.21.6905), X64 RyuJIT
Job-HJNSVS : .NET Framework 4.8 (4.8.4311.0), X64 RyuJIT
Job-FBCCVP : .NET Core 3.1.12 (CoreCLR 4.700.21.6504, CoreFX 4.700.21.6905), X64 RyuJIT
Method | Runtime | Mean | Error | StdDev | Ratio | Code Size | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|---|---|
CheckSetBit | .NET 4.8 | 0.7993 ns | 0.0060 ns | 0.0050 ns | 0.09 | 1378 B | - | - | - | - |
WaitZero | .NET 4.8 | 9.0637 ns | 0.0317 ns | 0.0296 ns | 0.97 | 1364 B | - | - | - | - |
Wait | .NET 4.8 | 9.3205 ns | 0.0463 ns | 0.0434 ns | 1.00 | 1338 B | - | - | - | - |
CheckSetBit | .NET Core 3.1 | 1.2812 ns | 0.0023 ns | 0.0019 ns | 0.29 | 65 B | - | - | - | - |
WaitZero | .NET Core 3.1 | 4.3391 ns | 0.0108 ns | 0.0096 ns | 0.98 | 1300 B | - | - | - | - |
Wait | .NET Core 3.1 | 4.4167 ns | 0.0163 ns | 0.0153 ns | 1.00 | 1274 B | - | - | - | - |
The lesson here was, that if you have a ManualResetEventSlim that will most of the time be set and it is being checked in a high-throughput scenario you should first check the IsSet property before deciding to call Wait, because even though Wait does do the IsSet check itself, it still requires a method call to Wait which is more expensive than simply checking the IsSet bit first.
Hope this helps!