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!

Mastodon