struct
Performance with Equality
Imagine you want to compare two structs with each other. In most cases this does work out of the box without any addition from you. As structs are value types they implicitly derive from System.ValueType
. And System.ValueType
offers some ways for checking equality.
Here is the basic idea:
- If there is no
GetHashCode
andEquals
use those methods fromSystem.ValueType
System.ValueType
takes either the first nullable type and checks ifother
is also null or it takes the first non-nullable type and callsEquals
for that
But that's not all. First it does the following
// if there are no GC references in this object we can avoid reflection
// and do a fast memcmp
if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj);
FieldInfo[] thisFields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
So under some circumstances we have a fast-track. But this only happens when there is no referencetype involved and the memory layout fits. Otherwise we run into reflection. That can cost some precious performance.
If you need a lot of equality checks of your struct it is wise to implement IEquatable
. We will see how this works a bit later
C# 10 to the rescue
C# 10 will introduce record struct
s. So we gain IEquatable
directly from the compiler. This also means we get implcitly GetHashCode
. Why this is important I will show later.
Performance
Let's have a look at this 3 structs:
public struct MyStruct
{
public int A { get; init; }
}
public struct MyStructWithEquality : IEquatable<MyStructWithEquality>
{
public int A { get; init; }
public bool Equals(MyStructWithEquality other) => A == other.A;
}
public readonly record struct MyReadonlyRecordStruct
{
public int A { get; init; }
}
All of them have one property. But our default struct has no implementation neither for Equals
nor GetHashCode
.
PS: To make this example working with .net5/.net6 preview you have to add the following package-reference: <PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.0.0-2.final">
Your IDE will still show issues but it is compilable.
Update 30th November 2021: With the release of .NET6 and C# 10 readonly struct
are not anymore dependent on any external Nuget package.
Let's have a small performance case:
public class Benchmark
{
private MyStruct _myStruct1 = new MyStruct() {A = 2};
private MyStruct _myStruct2 = new MyStruct() {A = 3};
private MyStructWithEquality _myStructWithEquality1 = new MyStructWithEquality() {A = 2};
private MyStructWithEquality _myStructWithEquality2 = new MyStructWithEquality() {A = 3};
private MyReadonlyRecordStruct _myReadonlyRecordStruct1 = new MyReadonlyRecordStruct() {A = 2};
private MyReadonlyRecordStruct _myReadonlyRecordStruct2 = new MyReadonlyRecordStruct() {A = 3};
[Benchmark(Baseline = true)]
public bool MyStructEqual() => _myStruct1.Equals(_myStruct2);
[Benchmark]
public bool MyStructWithEqual() => _myStructWithEquality1.Equals(_myStructWithEquality2);
[Benchmark]
public bool MyReadonlyRecordStructEqual() => _myReadonlyRecordStruct1.Equals(_myReadonlyRecordStruct2);
}
Here are the results:
Method | Mean | Error | StdDev | Median | Ratio |
---|---|---|---|---|---|
MyStructEqual | 20.8457 ns | 0.4447 ns | 1.1793 ns | 20.5156 ns | 1.000 |
MyStructWithEqual | 0.0206 ns | 0.0249 ns | 0.0341 ns | 0.0000 ns | 0.001 |
MyReadonlyRecordStructEqual | 0.0339 ns | 0.0246 ns | 0.0462 ns | 0.0126 ns | 0.002 |
We see how drastic our own implementation is.
The same applies to GetHashCode and especially when using your struct as a key in a dictionary. So we'll explicity provide a GetHashCode
function for our MyStructWithEquality
public override int GetHashCode() => A.GetHashCode();
Our readonly record struct will have it automatically. Now let's run the benchmark:
[Benchmark(Baseline = true)]
public int MyStructEqual() => _myStructDict[_myStruct];
[Benchmark]
public int MyStructWithEqual() => _myStructDictWithEquality[_myStructWithEquality];
[Benchmark]
public int MyReadonlyRecordStructEqual() => _myReadonlyDict[myReadOnlyStruct];
Method | Mean | Error | StdDev | Ratio |
---|---|---|---|---|
MyStructEqual | 58.677 ns | 1.1585 ns | 1.1897 ns | 1.00 |
MyStructWithEqual | 6.902 ns | 0.1742 ns | 0.3397 ns | 0.12 |
MyReadonlyRecordStructEqual | 8.048 ns | 0.1198 ns | 0.0935 ns | 0.14 |
We see it can make a huge difference
Conclusion
Be aware of the situation and the implications it can have to provide an implementation for Equals
and GetHashCode
. If you use it often it can bring performance improvements.