struct Performance

01/08/2021

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 and Equals use those methods from System.ValueType
  • System.ValueType takes either the first nullable type and checks if other is also null or it takes the first non-nullable type and calls Equals for that

But that's not all. First the 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 structs. 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.

2
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x