Analyzing Performance Issues in Microsoft .NET 8

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 using new() (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 using CreateInstance<> (as illustrated below) is 1.12 times slower.
  • Utilizing CreateInstance<> with typeof() to create a Person object (as depicted below) exhibits a slowdown of 1.18 times.
  • Creating a Person structure with CreateInstance<> and typeof() 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:

  • ==‘ with ToLower(InvariantCulture) is 1.07 times slower.
  • ==‘ with ToLowerInvariant() is 1.05 times slower.
  • ==‘ with ToUpperInvariant() is 1.03 times slower.
  • ==‘ with ToUpper(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 a StringBuilder from an ObjectPool, which is 1.09 times slower.
  • AppendFormat, which is 1.12 times slower.
  • AppendFormat when using a StringBuilder from an ObjectPool, which is 1.1 times slower.
  • Insert when using a StringBuilder from an ObjectPool, 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.

2 thoughts on “Analyzing Performance Issues in Microsoft .NET 8

  1. 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

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.