Enum.Equals - Performance analysis

1/11/2022
6 minute read

Imagine we have this very enum:

public enum Color
{
    Red = 0,
    Green = 1,
}

Nothing really fancy but for us it is enough. We have multiple ways of comparing if two instances of an enum are the same. But before I dive into some explanation I'll show you the results upfront with the benchmark code:

public class Benchmark
{
    private readonly Color _colorRed = Color.Red;
    private readonly Color _colorGreen = Color.Green;

    [Benchmark(Baseline = true)]
    public bool ObjectEquals() => Equals(_colorRed, _colorGreen);

    [Benchmark]
    public bool EnumEquals() => Enum.Equals(_colorRed, _colorGreen);

    [Benchmark]
    public bool InstanceEquals() => _colorRed.Equals(_colorGreen);

    [Benchmark]
    public bool ComparisonOperator() => _colorRed == _colorGreen;
}

We have 4 options to compare:

  • object.Equals
  • Enum.Equals
  • Call Equals from on the instance method
  • Use the comparison operator ==

Now bring in the results:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1348 (21H1/May2021Update)
Intel Core i7-7820HQ CPU 2.90GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.101
  [Host]     : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT
  DefaultJob : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT


|             Method |      Mean |     Error |    StdDev |    Median | Ratio | RatioSD |
|------------------- |----------:|----------:|----------:|----------:|------:|--------:|
|       ObjectEquals | 9.1618 ns | 0.2113 ns | 0.1765 ns | 9.1957 ns | 1.000 |    0.00 |
|         EnumEquals | 9.1438 ns | 0.1075 ns | 0.0839 ns | 9.1245 ns | 0.997 |    0.02 |
|     InstanceEquals | 9.9292 ns | 0.2370 ns | 0.5858 ns | 9.7626 ns | 1.086 |    0.10 |
| ComparisonOperator | 0.0445 ns | 0.0353 ns | 0.0571 ns | 0.0250 ns | 0.008 |    0.01 |

What the duck! Basically only the comparison operator takes only 1/100 of the time as the other variations!

Now let's find out why!

Boxing and unboxing

Now the first two approaches have the same runtime (considering the error-rate). Why is that? Well EnumEquals and ObjectEquals are the same. Even our IDE will give us a hint here:

EnumEquals

We can see that the Enum qualifier is redundant. But why is that. Well here we go:

Enum object equals

When we hover over Enum.Equals as well as Equals we get the hint that this is invoked from object. So what does object.Equals do here? It is quite simple:

public static bool Equals(object? objA, object? objB)
{
    if (objA == objB)
    {
        return true;
    }
    if (objA == null || objB == null)
    {
        return false;
    }
    return objA.Equals(objB);
}

Hold on the first line looks like our last approach, which is the fastest! So did I manipulate the test because my enums are not the same and therefore the runtime is way longer. No. Even if we would this:

[Benchmark]
public bool InstanceEqualsWhenTheSame() => _colorRed.Equals(_colorRed);

We'd get this:

|                    Method |     Mean |     Error |    StdDev |
|-------------------------- |---------:|----------:|----------:|
| InstanceEqualsWhenTheSame | 9.821 ns | 0.1284 ns | 0.1138 ns |

The same runtime! So we see that all approaches with Equals are the same and only the last one is somehow different. Let's have a look at the IL-code.

This Equals(_colorRed, _colorGreen); as well as this _colorRed.Equals(_colorGreen); will roughly translate to the same IL-code:

IL_0005: ldloc.0
IL_0006: box C/Color
IL_000b: ldloc.1
IL_000c: box C/Color
IL_0011: call bool [System.Private.CoreLib]System.Object::Equals(object, object)
IL_0016: pop

The shown example above is from Equals(_colorRed, _colorGreen);

Now IL_0006: box C/Color and IL_000c: box C/Color are interesting! We have to box our enums!

How does it look for our == case?

IL_002b: ldloc.0
IL_002c: ldloc.1
IL_002d: ceq
IL_002f: stloc.2

Yes you see right. There is no boxing. Just a simple equality comparison ceq. By the way if you want to play around on your own: sharplab.io

Boxing is the process of converting a value type to the type object or to any interface type implemented by this value type. When the common language runtime (CLR) boxes a value type, it wraps the value inside a System.Object instance and stores it on the managed heap. Unboxing extracts the value type from the object. Boxing is implicit; unboxing is explicit.

From the official Microsoft documentation

Boxing a value type into a reference type costs a bit time. And this is exactly why it is so more expensive. If you want to know more about boxing / unboxing I'd suggest to read the link above.

If you want to have another example of boxing/unboxing here you go. Here I discussed boxing and unboxing in string interpolation.

The repository can be found: here Central repository with a lot of samples discussed in this blog can be found https://github.com/linkdotnet/BlogExamples

struct Performance

Let's have a small look into structs and how they work when using Equals and GetHashCode. Plus have a brief look into a new C# 10 feature: readonly record struct.

A story about boxing / unboxing and string interpolation in C#

A story about boxing / unboxing and string interpolation in C#. What has string interpolation to do with boxing / unboxing and what's the impact?

Heap, Stack, Boxing and Unboxing, Performance ... let's order things!

In this article I will shade some lights on some of the most used terms which seems very confusing especially for beginners: heap, stack and boxing and unboxing.

Furthermore we will also encounter internet wisdom like:

Value types get stored on the stack. Reference types on the heap

We discuss why this is wrong and what the hell performance has to do with it?

An error has occurred. This application may no longer respond until reloaded. Reload x