Introduction
Since the release of .NET 6 in November 2021, we have witnessed a rapid evolution in the .NET lineup. Within three years, .NET 7, .NET 8 and now, in November 2024, .NET 9 are on the doorstep. This rapid succession of updates raises the question: is it really worth upgrading to the latest version? What impact do these upgrades have on the performance of our applications? In this article, we take a close look at the performance improvements in .NET 6, .NET 8 and .NET 9 to discuss whether an upgrade is actually worthwhile for your projects or clients.
The central question for us is always:
Can I expect a noticeable speedup in my application when switching to the latest .NET version, without needing to modify my code?
In this post we compare the performance improvements across .NET 6, .NET 8 and .NET 9, based on official benchmarks and tests from Microsoft. We want to find out whether the latest .NET updates deliver real added value.
Environment
To compare the performance improvements in .NET 6, .NET 8 and .NET 9, we used the official benchmarks from Microsoft and the .NET Benchmarking Library for our own tests.
All benchmarks were run on a Windows 11 system with a 13th Gen Intel Core i9-13900H. The .NET versions used for testing are:
- .NET 6.0 : .NET 6.0.33 (6.0.3324.36610), X64 RyuJIT AVX2
- .NET 8.0 : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2
- .NET 9.0 : .NET 9.0.0 (9.0.24.43107), X64 RyuJIT AVX2
What was compared?
There were many performance improvements in .NET 6, .NET 8 and .NET 9, but many of them fall into niches that are not relevant for all applications. LINQ, JSON and Reflections are, however, important building blocks of modern applications, so we decided to compare these.
LINQ
We start with LINQ, which has often been criticised for its performance in the past. We tested the following LINQ operations ...
public class LinqBenchmarks
{
private static readonly IEnumerable<int> _range = Enumerable.Range(0, 1000);
private readonly IEnumerable<int> _list = _range.ToList();
private readonly IEnumerable<int> _arrayDistinct = _range.ToArray().Distinct();
private readonly IEnumerable<int> _appendSelect = _range.ToArray().Append(42).Select(i => i * 2);
private readonly IEnumerable<int> _rangeReverse = _range.Reverse();
private readonly IEnumerable<int> _listDefaultIfEmptySelect = _range.ToList().DefaultIfEmpty().Select(i => i * 2);
private readonly IEnumerable<int> _listSkipTake = _range.ToList().Skip(500).Take(100);
private readonly IEnumerable<int> _rangeUnion = _range.Union(Enumerable.Range(500, 1000));
[Benchmark] public bool Any() => _list.Any(i => i == 1000);
[Benchmark] public bool All() => _list.All(i => i >= 0);
[Benchmark] public int Count() => _list.Count(i => i == 0);
[Benchmark] public int First() => _list.First(i => i == 999);
[Benchmark] public int Single() => _list.Single(i => i == 0);
[Benchmark] public int DistinctFirst() => _arrayDistinct.First();
[Benchmark] public int AppendSelectLast() => _appendSelect.Last();
[Benchmark] public int RangeReverseCount() => _rangeReverse.Count();
[Benchmark] public int DefaultIfEmptySelectElementAt() => _listDefaultIfEmptySelect.ElementAt(999);
[Benchmark] public int ListSkipTakeElementAt() => _listSkipTake.ElementAt(99);
[Benchmark] public int RangeUnionFirst() => _rangeUnion.First();
}
... and summarised the most interesting results:
The results show that the performance of LINQ operations in .NET 9 has improved significantly compared to .NET 6 and .NET 8. If the upgrade from .NET 6 to .NET 8 was already enormous, the jump from .NET 8 to .NET 9 is even more impressive.
JSON
JSON is another important building block of modern applications. We tested the performance of JSON serialisation and deserialisation in .NET 6, .NET 8 and .NET 9.
public class JsonBenchmarks
{
private static readonly JsonSerializerOptions s_options = new()
{
Converters = { new JsonStringEnumConverter() },
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
};
[Params(BindingFlags.Default, BindingFlags.NonPublic | BindingFlags.Instance)]
public BindingFlags _value;
private byte[]? _jsonValue;
private Utf8JsonWriter _writer = new(Stream.Null);
[GlobalSetup]
public void Setup() => _jsonValue = JsonSerializer.SerializeToUtf8Bytes(_value, s_options);
[Benchmark]
public void Serialize()
{
_writer.Reset();
JsonSerializer.Serialize(_writer, _value, s_options);
}
[Benchmark]
public BindingFlags Deserialize() =>
JsonSerializer.Deserialize<BindingFlags>(_jsonValue, s_options);
}
The results of the JSON benchmarks are as follows:
The performance of JSON serialisation and deserialisation has improved in .NET 9 compared to .NET 6 and .NET 8.
Reflections
Even though Reflections should be avoided in modern applications, there are still cases where they are necessary.
public class ReflectionBenchmarks
{
private static object s_staticReferenceField = new object();
private object _instanceReferenceField = new object();
private static int s_staticValueField = 1;
private int _instanceValueField = 2;
private object _obj = new();
private static readonly Type _type = typeof(ReflectionBenchmarks);
private readonly FieldInfo _staticReferenceFieldInfo = _type
.GetField(nameof(s_staticReferenceField), BindingFlags.NonPublic | BindingFlags.Static)!;
private readonly FieldInfo _instanceReferenceFieldInfo = _type
.GetField(nameof(_instanceReferenceField), BindingFlags.NonPublic | BindingFlags.Instance)!;
private readonly FieldInfo _staticValueFieldInfo = _type
.GetField(nameof(s_staticValueField), BindingFlags.NonPublic | BindingFlags.Static)!;
private readonly FieldInfo _instanceValueFieldInfo = _type
.GetField(nameof(_instanceValueField), BindingFlags.NonPublic | BindingFlags.Instance)!;
[Benchmark] public object? GetStaticReferenceField() => _staticReferenceFieldInfo.GetValue(null);
[Benchmark] public void SetStaticReferenceField() => _staticReferenceFieldInfo.SetValue(null, _obj);
[Benchmark] public object? GetInstanceReferenceField() => _instanceReferenceFieldInfo.GetValue(this);
[Benchmark] public void SetInstanceReferenceField() => _instanceReferenceFieldInfo.SetValue(this, _obj);
[Benchmark] public int GetStaticValueField() => (int)_staticValueFieldInfo.GetValue(null)!;
[Benchmark] public void SetStaticValueField() => _staticValueFieldInfo.SetValue(null, 3);
[Benchmark] public int GetInstanceValueField() => (int)_instanceValueFieldInfo.GetValue(this)!;
[Benchmark] public void SetInstanceValueField() => _instanceValueFieldInfo.SetValue(this, 4);
}
Notably here, .NET 8 delivers partially slower results than .NET 6. .NET 9, on the other hand, shows a marked improvement across the board.
Conclusion
The performance improvements in .NET 9 are impressive. The benchmarks show that .NET 9 offers a significant performance boost in many areas. If you are looking for a way to improve the performance of your applications, upgrading to .NET 9 can be a good option without requiring any code changes. It is important to note, however, that results may vary depending on the application. Your own testing and benchmarking is therefore essential.
Need support?
Want to upgrade to .NET 9 or optimise the performance of your .NET applications but unsure where to start? We are happy to help! Simply get in touch via our contact page and we will work together to make your applications faster and more efficient.
Source code
The complete source code for the benchmarks is available on GitHub.
