Interesting Performance Implications of C# 9 Records Equality Check
Recently, .NET 5.0 RC1 was announced, so I decided to look into C# 9 Records. I played with them while in a preview, but thought I’ll give it a go once it enters RC, as in preview… well it’s a preview 😊
This is not a blog post on what C# 9 Records are and what features they provide. But we’ll cover some basics for the needs of this post.
Records basics
There is a new keyword introduced in C# 9: record. This allows us to define a Record
public record Money(decimal Value, string Currency);
That’s it, this is all you need to declare a record. During compilation a lot of things will be generated for us:
A class named Money, which
- Implements IEquatable<Money> interface
- Defines init only Value / Currency properties
- Has a public constructor which accepts value and currency as arguments
- Overloads == / != operators
- Overrides Equals/ToString methods
- and more…
With just 1 line of code I can have an immutable class, with value semantics (2 different instances are equal if their contents are equal). So I can write code like this
This is super exciting ! I’m actually using this kind of constructs quite a lot and if I don’t have to implement all this value semantics, that’s going to be a productivity boost.
One area I was curious about is performance. How this auto generated equality compares to custom implementation ?
Let’s look at a scenario where we have an immutable construct, which has value semantics. First I’ll define it as C# 9 Record, then I’ll have a custom implementation and will compare Equals() method performance on both.
Set the stage for stock price
Assume we have a stock price, which is defined as a pair of stock symbol & price.
I define RSymbol record to represent stock’s symbol and RStockPrice record which combines stock symbol & price.
I can now do this
Let’s implement the same manually.
First let’s look into CSymbol (note I use R prefix for Records & C prefix for Custom)
Now, let’s implement CStockPrice
(In both cases I omitted overloading of ==/!= operators to keep it concise, I’m not going to use them in benchmarks anyways).
Having both Records & custom implementations, we can now create benchmarks.
Equality Benchmarks
I’m using BenchmarkDotNet to create and execute benchmarks. This is the benchmark I’m going to use for equality tests
Executing it produces the following output
Wow, I was expecting a difference, but not 70 % !
However, what we have here is StockPrice Equals method calling Symbol’s Equals method, how about comparing single equal method call for Record & Custom implementation (which in benchmarks I’m calling Class) ?
So here’s the benchmark I’m going to run
As you can see the difference is still big ~ 50 % !
In Closing
As we’ve seen there is a big performance difference between custom implementation & Record’s equality comparison. Does this mean custom implementation should be favored over Records ? I’d say only when you have performance issues, you diagnose, analyze, come to a conclusion that the bottleneck is Equals method call on the Record, then look into custom implementation path.
In all other cases Records should be favored, in my opinion.