Over the past few months, I have been conducting extensive performance benchmarking on .NET 8 with the aim of sharing my findings at conferences such as the Copenhagen Developers Festival and BuildStuff{}. Additionally, I am in the process of updating performance-related articles on dotNetTips.com. .NET 8 has undoubtedly showcased remarkable improvements in performance, and the dedicated efforts of the .NET team have positioned it as one of the most performant programming languages available today.
However, during my review of the benchmark data, I identified certain areas where performance has regressed compared to .NET 6, the last long-term support version. While many of these findings do not exhibit a significant drop in performance, it’s crucial to acknowledge that when using methods demonstrated in this article that are invoked possibly thousands of times per second, the cumulative effect of these performance losses can become substantial. It’s worth emphasizing that as code execution duration increases in the cloud environment, your company’s cloud costs may escalate accordingly. I’ve taken the liberty to exclude any differences of 1 nanosecond or less from this analysis.
Object Creation
In .NET 8, there appears to be a slight slowdown in object creation. Here is the detailed breakdown:
- Creating a
System.Object
is now 1.8 times slower. - Creating an
Person
object is now 1.27 times slower. - Creating an
Person
object usingnew()
(as shown below) is now 1.29 times slower. - Creating a
Person
record object is 1.03 times slower.
Person person = new();
Creating an Instance of a Type using CreateInstance
The process of using CreateInstance<>
to generate objects has experienced a decline in speed. The breakdown of the slowdown is outlined below:
- Creating an
Person
object usingCreateInstance<>
(as illustrated below) is 1.12 times slower. - Utilizing
CreateInstance<>
withtypeof()
to create a Person object (as depicted below) exhibits a slowdown of 1.18 times. - Creating a
Person
structure withCreateInstance<>
andtypeof()
introduces a slowdown of 1.25 times.
Activator.CreateInstance(typeof(Person));
Activator.CreateInstance<Person>();
Disposing of Objects
The process of disposing of objects through different methods has also experienced a decline in speed. The breakdown is outlined below:
- Employing
try
/finally
to dispose of an object is 1.28 times slower. - Utilizing the
using
statement for object disposal exhibits a slowdown of 1.22 times. - Adopting the new style of
using
(as illustrated below) to dispose of an object proves to be 1.3 times slower.
using var dataTable = new DataTable();
Checking for Null
In .NET, because of the characteristics of reference types, it’s essential to always verify that an object is not null before attempting to invoke methods on it. If you have variables defined as follows:
DataTable table = null;
var testTable = new DataTable();
Performing null validation (as demonstrated below) results in a 1.31 times slower execution.
if (table == null)
{
testTable = table;
}
Performing null validation using the null coalescing operator (as demonstrated below) results in a 1.3 times slower execution.
table ??= testTable;
Array Creation
Creating an instance of an array is now 1.65 times slower. Furthermore, creating an array with CreateInstance
(as demonstrated below) is 1.24 times slower.
Array.CreateInstance(typeof(Person), 100);
Generating a Byte Array with Randomnumbergenerator
Creating a byte array using the RandomNumberGenerator
is significantly more efficient than utilizing Random
. Unfortunately, in .NET 8, it experiences a slowdown of 1.06 times.
Casting Types
The process of casting types using pattern matching has experienced a significant slowdown, now being 2.83 times slower. Additionally, casting types before pattern matching have also seen a slowdown, albeit to a lesser extent, at 1.05 times slower.
String Handling Methods
.NET 8 offers a range of string-handling methods, among which, the StringBuilder
has encountered a performance regression. These methods are outlined below.
Finding Strings
There is performance degradation in the following IndexOfAny
methods:
The utilization of IndexOfAny()
and AsSpan()
results in a 1.27 times slowdown. Here is the benchmark test code used:
return LongTestString.AsSpan().IndexOfAny(new[] { '“', '#' });
Utilizing IndexOfAny
(as demonstrated below) exhibits a 1.3 times decrease in speed.
return LongTestString.IndexOfAny(new[] { '“', '#' });
Unexpectedly, EndsWith
using a character has now become 237 times slower!
String Comparisons
Several string comparison methods have experienced performance degradation. They include:
- ‘
==
‘ withToLower(InvariantCulture)
is 1.07 times slower. - ‘
==
‘ withToLowerInvariant()
is 1.05 times slower. - ‘
==
‘ withToUpperInvariant()
is 1.03 times slower. - ‘
==
‘ withToUpper(InvariantCulture)
is 1.06 times slower.
StringBuilder
In the latest release, several StringBuilder
methods have exhibited reduced performance. These include:
Append
is 1.11 times slower.Append
when using aStringBuilder
from anObjectPool
, which is 1.09 times slower.AppendFormat
, which is 1.12 times slower.AppendFormat
when using aStringBuilder
from anObjectPool
, which is 1.1 times slower.Insert
when using aStringBuilder
from anObjectPool
, which is 1.11 times slower.
String Concatenation
Some string concatenation methods can lead to a performance decrease.
Using string.Join()
within a loop (as demonstrated below) is approximately 1.43 times slower.
foreach (var item in stringArray)
{
result = string.Concat(result, item, ControlChars.Space);
}
String concatenation with string.Concat()
(as illustrated below) is roughly 1.05 times slower.
string.Concat(stringArray)
String concatenation with string.Join()
using string.Empty
(as demonstrated below) is approximately 1.07 times slower.
string.Join(string.Empty, stringArray)
String concatenation with string.Join()
using a space (as demonstrated below) is roughly 1.06 times slower.
string.Join(ControlChars.Space, stringArray);
Decoding and Encoding Strings
Certain decoding and encoding methods (as exemplified below) exhibit a performance decrease. Here is an example:
Encoding.ASCII.GetString(longTestString);
The methods with a performance loss are listed below:
- Decoding with ASCII is 1.3 times slower.
- Decoding with Default is 1.37 times slower.
- Decoding with Latin is 1.6 times slower.
- Decoding with UTF8 is 1.25 times slower.
- Encoding with ASCII is 1.31 times slower.
- Encoding with Default is 1.17 times slower.
- Encoding with Latin is 1.25 times slower.
- Encoding with UTF8 is 1.17 times slower.
LINQ Performance Considerations
Some LINQ APIs and Lambda methods exhibit performance degradation. Here are the methods, each accompanied by the code used in the benchmark test.
Utilizing Where()
for List<>
of record
types results in a 1.07 times decrease in performance.
personRecordList.Where(p => p.BornOn.UtcTicks > TickCount).ToList();
Using Where()
for List<>
of reference types is 1.08 times slower, while for a List<>
of value types, it’s 1.2 times slower.
personRefList.Where(p => p.BornOn.UtcTicks > TickCount).ToList();
Using Where()
and Any()
for List<>
of value types results in a 1.07 times decrease in performance.
personValList.Where(p => p.BornOn.UtcTicks > TickCount).Any();
Using Where()
in conjunction with FirstOrDefault()
for a List<>
of value types results in a 1.04 times decrease in performance.
personValList.Where(p => p.BornOn.UtcTicks > TickCount).FirstOrDefault();
Using Any()
with the LINQ API for a List<>
of value types is 1.04 times slower.
query = from person in personValList where
person.BornOn.UtcTicks > TickCount select person;
return query.Any();
Using the LINQ API (as demonstrated below) for a List<>
of record
types is 1.05 times slower. Furthermore, for a List<>
of reference types, it’s 1.08 times slower.
query = from person in personRecordList
where person.BornOn.UtcTicks > TickCount select person;
return query.ToList();
In Conclusion
There you have it. I’ve demonstrated several ways in which .NET 8 exhibits performance degradation when compared to .NET 6. It’s important to be aware of this before making the decision to transition to .NET 8. Personally, I have already begun migrating the code for my Open-Source Software (OOS) project, Spargine, which I hope to release soon. Keep in mind that your results may vary, and I strongly recommend using BenchmarkDotNet to benchmark your code.
If you’re interested in the specifics of how I benchmark code, please check out my article on benchmarking, titled “Benchmark Your Code Like dotNetDave“. Additionally, I encourage you to explore the latest edition of my book, “Rock Your Code: Code & App Performance for Microsoft .NET,” which can be found on Amazon.com. For the most up-to-date performance information on .NET 8, please follow this link: https://bit.ly/CodeAppPerformance.
Feel free to share your thoughts and insights in the comments below. Your feedback is always welcome and appreciated.
But how this results was measured?
Where is the repo with sorces of this benchmarks, that show all of these degadations?
I can’t reproduce this results with BenchmarkDotNet and LINQPad
I don’t make this repository public since it’s for my book on code performance. If you would like to see how I benchmark .NET, check out this article: https://dotnettips.wordpress.com/2022/11/28/benchmark-your-code-like-dotnetdave/