For the past 5-6 years we’ve seen consistent improvements in performance and functionality across a multitude of products from Microsoft. Whether its SQL Server, Windows Server or even Windows 10, everything seems to be getting better and faster. Well the same has held true for the underlying technology which powers many of these experiences, .NET.
.NET Framework had grown to be a very robust system, but given its long history and evolution, it was somewhat saddled with legacy dependencies and compromises which began to hold back possibilities and even performance potential. One of the main goals of .NET Core was to break away from those limitations and give the framework a fresh start and open up horizons.
At the beginning of the .NET Core journey, it was becoming clear that we were in for a great ride. Out of the gate developers started to notice 2-3x improvements in performance and efficiency. There were obvious tradeoffs at the beginning, mostly due to the resultant feature gap, however that is no longer the case.
On the proverbial eve of .NET 6 being released, there is virtually nothing you can’t do with .NET 6 which would require you to tether yourself to the aging .NET Framework.
In this article we’re going to dive into some of the performance improvements realized by the forthcoming release of .NET. In some cases they might not seem significant when compared to .NET 5, but when compared to even the latest version .NET Framework or .NET Core 3.1 , we start to see just how far Microsoft has come and how these improvements translate to a better experience for developers and end users. I wish I could take credit for all the metrics we are going to discuss in this article, but all the credit goes to Stephen Toub, Partner Software Engineer, on the .NET Team. Stephen did an outstanding job benchmarking dozens of scenarios, however I am going to focus on three which are important to me, and perhaps important to you as well. Alright, let’s get to it!
Collections and LINQ
Clone() Dictionary
Whether you are loading data from a database, files or APIs to store, manipulate or present back to users. Improvements to the underlying processes which power these workloads can have tangible results. As observed below, creating one dictionary from another exhibits shows a substantial improvements.
private IEnumerable<KeyValuePair<string, int>> _dictionary = Enumerable.Range(0, 100).ToDictionary(i => i.ToString(), StringComparer.OrdinalIgnoreCase);
[Benchmark]
public Dictionary<string, int> Clone() => new Dictionary<string, int>(_dictionary);
Method | Runtime | Mean | Ratio |
---|---|---|---|
Clone | .NET Core 3.1 | 3.224 us | 1.00 |
Clone | .NET 5.0 | 2.880 us | 0.89 |
Clone | .NET 6.0 | 1.685 us | 0.52 |
Clone() SortedDictionairy
private IDictionary<string, int> _dictionary = new SortedDictionary<string, int>(Enumerable.Range(0, 100).ToDictionary(i => i.ToString(), StringComparer.OrdinalIgnoreCase));
[Benchmark]
public SortedDictionary<string, int> Clone() => new SortedDictionary<string, int>(_dictionary);
Method | Runtime | Mean | Ratio |
---|---|---|---|
Clone | .NET Framework 4.8 | 69.546 us | 1.00 |
Clone | .NET Core 3.1 | 54.560 us | 0.78 |
Clone | .NET 5.0 | 53.196 us | 0.76 |
Clone | .NET 6.0 | 2.330 us | 0.03 |
DistinctCount()
One very common and useful scenario is getting a unique value count from a large list which perhaps has some duplicates. Here we can once again see ~2x improvement over .NET 5.
private IEnumerable<string> _data = Enumerable.Range(0, 100_000).Select(i => i.ToString()).ToArray();
[Benchmark]
public int DistinctCount() => _data.Distinct().Count();
Method | Runtime | Mean | Ratio | Allocated |
---|---|---|---|---|
DistinctCount | .NET 5.0 | 5.154 ms | 1.04 | 5 MB |
DistinctCount | .NET 6.0 | 2.626 ms | 0.53 | 2 MB |
Encoding, Decoding and Cryptography
Not many modern applications can get away with not implementing cryptography in one way or another. Encoding and Decoding data is a very integral part of most data driven applications in virtually every industry.
CryptoStream()
Encoding and Decoding is something I deal with very often in my day job, when I migrated from .NET Framework 4.8 to .NET Core 3.1 I did not see much improvement in the performance or efficiency when Base64 encoding images. With the release of .NET 5 the leap was monumental. Not only is the performance incredible, but the efficiency of doing the work is almost too good to believe as you can see below.
private byte[] _data = Enumerable.Range(0, 10_000_000).Select(i => (byte)i).ToArray();
private MemoryStream _destination = new MemoryStream();
[Benchmark]
public async Task Encode()
{
_destination.Position = 0;
using (var toBase64 = new ToBase64Transform())
using (var stream = new CryptoStream(_destination, toBase64, CryptoStreamMode.Write, leaveOpen: true))
{
await stream.WriteAsync(_data, 0, _data.Length);
}
}
Method | Runtime | Mean | Ratio | Allocated |
---|---|---|---|---|
Encode | .NET Framework 4.8 | 329.871 ms | 1.000 | 213,976,944 B |
Encode | .NET Core 3.1 | 251.986 ms | 0.765 | 213,334,112 B |
Encode | .NET 5.0 | 146.058 ms | 0.443 | 974 B |
Encode | .NET 6.0 | 1.998 ms | 0.006 | 300 B |
Blazor WASM
Any improvements to running .NET in the browser is music to my ears! If you don’t already know what Blazor WASM is, check Microsoft’s Blazor page. For this test, the sample Blazor app’s counter page was modified to SHA-265 hash a string a few thousand times. The results are pretty amazing. One of the biggest improvements come from the ability to Ahead-Of-Time compile Blazor WASM applications.
Now with .NET 6, a Blazor WASM app can be compiled ahead of time entirely to WebAssembly, avoiding the need for JIT’ing or interpreting at run-time. All of these improvements together lead to huge, cross-cutting performance improvements for Blazor WASM apps when targeting .NET 6 instead of .NET 5.
– Stephen Toub, .NET Blog
HashData()
@page "/counter"
@using System.Security.Cryptography
@using System.Diagnostics
@using System.Text
<h1>Hashing</h1>
<p>Time: @_time</p>
<button class="btn btn-primary" @onclick="Hash">Click me</button>
@code {
private const string Sonnet18 =
@"Shall I compare thee to a summer’s day?
Thou art more lovely and more temperate:
Rough winds do shake the darling buds of May,
And summer’s lease hath all too short a date;
Sometime too hot the eye of heaven shines,
And often is his gold complexion dimm'd;
And every fair from fair sometime declines,
By chance or nature’s changing course untrimm'd;
But thy eternal summer shall not fade,
Nor lose possession of that fair thou ow’st;
Nor shall death brag thou wander’st in his shade,
When in eternal lines to time thou grow’st:
So long as men can breathe or eyes can see,
So long lives this, and this gives life to thee.";
private TimeSpan _time;
private void Hash()
{
byte[] bytes = Encoding.UTF8.GetBytes(Sonnet18);
var sw = Stopwatch.StartNew();
for (int i = 0; i < 2000; i++)
{
_ = SHA256.HashData(bytes);
}
_time = sw.Elapsed;
}
}
Method | Runtime | Result | Improvement |
---|---|---|---|
Hash | .NET 5 JIT | 0.454 ms | |
Hash | .NET 6 JIT | 0.280 ms | 38 % |
Hash | .NET 6 AOT | 0.017 ms | 96 % |
For a ton of other benchmarks and information check out the source article here.
What do you guys think? Are you excited about .NET 6? Have you already started using it? Let me know your thoughts in the comments below.