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:
We can see that the Enum
qualifier is redundant. But why is that. Well here we go:
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