Performance improvements
Starting from .NET Core, since it was rewritten separately to .NET Framework, the main focus of .NET was performance, meaning performance was one of the most important features of the platform and, ever since then, each .NET version has enhanced performance further in every aspect of the platform.
Why performance is so important?
For any sizeable application, naturally, performance is a major area where non-functional improvements take place. Once all the processes and algorithms specific to the applications have been enhanced, there is not so much left to change that can add further performance enhancements. In such situations, analysis is required for every bit of code in minute detail. If performance improves by even just 0.001%, this may have a significant impact.
Let's have a few examples to build the context of how important performance improvements are that automatically affect the business and, hence, impact the functional part of the application.
According to independent statistics accumulated, as well as our own years of experience related to our online digital life that has become so normal these days, we can make the following general observations:
- More than 70% of customers who are dissatisfied with the performance of the web store are less likely to buy again and would rather seek alternatives next time.
- 1 out of 5 customers are likely to forego a purchase if the shopping cart and checkout process are very slow; some may even have a mistrust of the process.
- If a website is performing poorly, then no matter what type of services it provides, 30% of users would have a negative impression.
- Half of users expect the website to load in 2 seconds, especially on smartphones, and would leave the site if it takes more than 5 seconds to load.
- Amazon calculated that they would lose $1.6 billion every year if they slowed down by even just 1 second.
- Customers have a general perception that if it is fast, it must be professional!
- The rise of cloud computing means per-second billing of on-demand compute and memory resource consumption.
Performance stats by Skilled
Look here for stats from Skilled on how page speed affects e-commerce:https://skilled.co/resources/speed-affects-website-infographic/.
Seeing these real-world examples, we realize how vital performance improvements are for any online business presence. Even a second's improvement in the response time of a web application can generate significant profit!
These are the kinds of use cases as to why Microsoft has taken .NET to another level by focusing on improvements in every area and to every bit of code.
This means that if the underlying engine of the application code is supercharged, the application automatically reaps the performance benefits and, hence, the return on investments (ROI) gets increased as the company invests time and money in migrating to .NET 5.
Let's now see what are the main areas that have been improved in various versions of .NET, up to .NET 5, that are key to a faster application!
Performance improvements in .NET versions
Let's now look in depth at where .NET has improved performance, starting with .NET Core and going through to .NET 5. What we will do is examine the key focal points as regards improvements in .NET Core version by version. This will give us a good picture of how and where core performance improvements take place and how we automatically accrue the benefit of it in our applications.
By way of a general understanding based on the work of Microsoft and open source developers, all the newer versions of .NET Core (and now .NET from version 5.0) are built on top of previous versions, which means either they improve on top of existing performance tweaks or they even go back and increase performance gains, which, in the previous version, were improved in a slightly different manner. Further improvements in a newer version of an already improved feature are made possible due to the two main components:
- The first is following the availability of new C# language features (from C# version 7.3+), along with its improved compiler, which generates better code, for example, by utilizing techniques that take into account the best use case and also common scenarios.
- The second is owing to improvements in JITTER, which generates more efficient machine code by reducing the total number of instructions and using intrinsic instructions.
From the very first version of .NET Core, performance started to improve, and this held true for each version that followed. Let's visit the major improvement areas in order of the released versions.
Performance improvements in .NET Core
Performance improvements in .NET Core (for versions 1.0–2.1) initially focused on the following areas:
- Collections are faster as well as LINQ.
- Compression (specifically,
Deflate Stream
). - Cryptography and math (specially
BigInteger
). - Serialization (
BinaryFormatter
improved in .NET Core 2.0, but generally it is recommended to use other types of serialization). - Text processing (improvement in
RegEx
,UrlDecoding
– a minimum of 50% faster). - Improvements in strings (
Equals
,IndexOf
,IndexOfAny
,Split
, andConcat
). - Asynchronous file streaming (
CopyToAsync
is 46% faster). - Networking (especially sockets, improvements in asynchronous read, write, and copy operations in
NetworkStream
operations as well asSslStream
). - Concurrency (threadpool, synchronization, especially
SpinLock
andLazy<T>
). - JIT (devirtualization and often method inlining)
Performance improvements in .NET Core 3
Now, let's check major improvement areas in .NET Core version 3:
- Various improvements associated with manipulating data in memory, especially with span and memory.
- Improvements in arrays and strings (based on the use of spans and their vectorization optimizations).
- Collections are faster (
Dictionary
,Queue
,BitArray
, andSortedSet
are around 50% faster than in .NET Core 2.1). System.Decimal
has been overhauled and is much faster compared with .NET Core 2.0.- Threading (tasks and async are faster, timers are optimized).
- Networking (an even faster
SslSteam
, HttpClient: a larger buffer size, thereby reducing system calls to transfer data). - JIT's direct access to newer and compound CPU instructions.
- Many I/O improvements that are at least 25% faster than .NET Core 2.1.
- Improvements in the interop, which itself is used by .NET (
SafeHandle
50% faster). - Improvements in the Garbage Collector (GC) (set memory limits, and more container-aware).
Performance improvements in .NET 5
Finally let's overview the major performance improvements areas in .NET 5 that are on top of .NET Core's aforementioned improvements:
- Garabage Collection (GC): GC has a process of marking the items that are in use. Server GC allocates one thread per core for the collection process. When one thread has finished marking all the items, it will continue to work on marking the items that have not yet been completed by other threads. In this way, it speeds up the overall collection process.
GC is optimized to decommit the Gen0 and Gen1 segments upon which it can return the allocated memory pages back to the operating system.
Improvements in the GC's scalability on machines with a higher number of cores reduces memory resets in low-memory situations.
Movement in some of the code, such as sorting primitives from C code into C#, also helped in managing the runtime and in the further development of APIs, as well as in helping to reduce the GC Pause, which ultimately improved the performance of the GC as well as the application. GC Pause is a pause time, which means how long the GC must pause the runtime in order to perform its work.
- Hardware Intrinsics: .NET Core 3.0 added lots of hardware intrinsics that allow JIT to enable C# code to directly target CPU instructions, such as SSE4 AVX. .NET 5.0 also added a lot more intrinsics specific to ARM64.
- Text Processing:
char.IsWhiteSpace
has been improved, thereby requiring a smaller number of CPU instructions and less branching. Improvements in this area have also facilitated improvements in lots of string methods, such asTrim()
.char.ToUpperInvariant
is 50% faster than .NET 3.1, whileInt.ToString
is also 50% faster.Further improvements have been made in terms of encoding, for example,
Encoding.UTF8.GetString
. - Regular Expressions:
RegEx
has seen more than a 70% performance improvement compared to .NET Core 3.1 in various cases. - Collections: Lots of improvements have been made to
Dictionary
, especially in the lookups and utilizing theref
returns to avoid a second lookup, as the user would pick up the value when obtaining the keys with the computed hash.Similar upgrades applied to
ConcurrentDictionary
as well.Hashset
wasn't optimized previously, as wasDictionary
, but now it is optimized on a similar algorithm toDictionary
, meaning it is much faster than .NET FW 4.8 and even .NET Core 3.1.Iteration on
ImmutableArray
is optimized by inlining theGetEnumerator
and further JIT optimization, and hence the iteration in .NET 5 is almost 4 times faster than 3.1.BitArray
is a specialized collection that was also optimized by the open source non-Microsoft developer by utilizing the hardware intrinsics using AVX2 and SSE2 advanced CPU instructions. - LINQ: Improvements in .NET 5 for LINQ made
OrderBy
15% andSkipLast
50% faster than 3.1. - Networking: Socket improvements were made to the Linux platform for faster asynchronous I/O with
epoll
and a smaller number of threads. A number of improvements to theSocket Connect
andBind
methods, along with underlying improvements, made them even faster than .NET Core 3.1.SocketsHttpHandler
andDate
format validation in the header is optimized, giving us more performance gains.HTTP/2 code is optimized as it was mostly functional in 3.1, but performance improvements took place in 5.0, which makes it perform twice as fast and consume almost half of the memory in certain scenarios.
- JSON: A number of improvements have been made to the
System.Text.JSON
library for .NET 5, especially forJsonSerializer
, which makes it more than 50% faster than 3.1 and means it consumes much less memory. - Kestrel: Kestrel is a web server included with .NET SDK. This web server is specifically designed to serve APIs built on .NET Core and .NET 5. It should be noted that as a result of performance improvements in the areas of reduced allocations in HTTP/2, along with the higher use of
Span
and improvements in GC, this has given a significant boost to the Kestrel implementation, which is included with .NET 5. These especially have a direct impact when serving gRPC-based APIs as well as REST APIs.
Wow! That was quite an impressive number of performance improvements and these, too, applied version by version to every level of the code. This many optimizations applied to such types of projects across this time duration is not something normally observed.All of this is the result of a number of expert Microsoft and other developers working on an open source project and we all reap the benefits from this in terms of our individual productivity.
Let's now look at a couple of examples to run the code and compare performance.
Let's do some benchmarking
In the previous section, we have seen a number of improvements applied to a plethora of feature sets. We have also seen that so far; .NET 5 is a superset of all the performance improvements and is the fastest .NET version out there.
At the end of this chapter, I have placed the links to the article where Microsoft has specified a number of benchmarks for each of the aspects they talked about. Like me, you can also run them very easily. Out of those, I will pick one and present it here, as well as the complete code, so that you can repeat the experiment on your own machine.
Benchmarking API performance between .NET versions
In the first benchmarking program, I will pick one of the benchmarking examples similar to the Microsoft Blog post for demonstration purposes and will also show the results as they ran on my machine. We will benchmark this with .NET Framework 4.8, .NET Core 3.1, and .NET 5.0:
- To begin, first of all, this is a tiny project setup with only one
Program.cs
file required as a console application. In this, we use the NuGet package used for all types of .NET benchmarking:benchmarkdotnet
. At the time of writing, I used version0.12.1
. - To execute the benchmark, just use this simple
.csproj
file:For our example, I am using here this
Program.cs
file, while in the code repository for this book, I have included a couple more that you can also use to test out quickly:On my machine, I have .NET Framework 4.8, .NET Core 3.1, and .NET 5 RC2 installed. So, in order to run these benchmarks successfully on your machine, please ensure that you install them too.
- Typing this command generates the following output:
C:\>dotnet –version 5.0.100-rc.2.20479.15
This confirms that .NET 5.0 is the dominant SDK on my machine and which, if required, is changeable via the
global.json
file.Tip
Placing this
global.json
file on the directory changes the default dotnet SDK for all of the sub-directories. For example, I place the following content in myglobal.json
file to change the default SDK/compiler for all subprojects to .NET Core 3.1:{
    "sdk": {
        "version": "3.1.402"
    }
}
- I placed the two files as mentioned earlier into the following folder:
C:\Users\Habib\source\repos\Benchmarks
. - Then, I run the following command to execute the benchmark:
dotnet run -c Release -f net48 --runtimes net48 netcoreapp3.1 netcoreapp5.0 --filter *Program*
The command is giving an instruction to build and run the code in the current directory using the entry point from
Program
and generate and execute three.exe
files, one each for .NET Framework 4.8, .NET Core 3.1, and .NET 5.0. - Running this command on my machine now, I get the following result, which I will paste here for our reference. Remember that results may vary depending on the kind of machine on which we execute our benchmark. I am including here only the last part of the execution output, which is the summary of the benchmarking execution:
Notice here the mean execution time in nanoseconds and, in the last column, the number of bytes allocated to each execution of the test case.
From the final output, it can be clearly seen that .NET 5 outperforms both .NET Core 3.1 and .NET Framework 4.8 by a wide margin. With this, let's now move on to our second benchmarking program.
Benchmarking a tiny Web API using .NET Core 3.1 and .NET 5.0
In the first benchmarking example, we saw a performance comparison of one .NET API: IndexOfAny
on the byte array, and how .NET 5 was the best of all. In this second example, we will create a very tiny Web API project, in fact, a default one from the dotnet scaffolding. We are then going to run it on localhost and call it 50,000 times and see the performance statistics in a manner that is almost akin to blackbox performance testing.
Note that this is not a realistic test in terms of a practical application, but it can give us an overall impression of the general performance of the same code using .NET Core 3.1 versus .NET 5.0 without applying any tweaks.
Setting up the benchmark
To begin our tiny benchmark, we first need to set up a few things. Here, I am listing them in short and easy steps:
- First, we set up our project structure so that the instructions run perfectly for all readers. So, first of all, my directory hierarchy looks like this:
C:\Users\Habib\source\repos\Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features\code\Chapter01
- Now, using the
global.json
file as mentioned in the previous tip, I set my dotnet SDK version to be .NET Core 3.1 by placing theglobal.json
file in theChapter01
folder. - Then, I execute the following commands:
dotnet –version dotnet new webapi -o dummyapi31
The first command is just to verify that the SDK is .NET Core 3.1, and the second command creates the default Web API project.
- After that, just edit this file on Notepad++:
C:\Users\Habib\source\repos\Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features/blob/master/Chapter01/dummyapi31/Controllers/WeatherForecastController.cs
- Then, add the following lines to the
Get()
method: - After this, execute the following commands to run the Web API project:
cd dummyapi31 dotnet run
And I have the following output:
info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Now listening on: https://localhost:5001 info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Application started. Press Ctrl+C to shut down.info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Hosting environment: Development info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Content root path: C:\Users\Habib\source\repos\Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features\code\Chapter01\dummyapi31
- Similarly, I do the same thing for the executable based on .NET 5.0. First, I disable the
global.json
file by renaming it, then I executedotnet –version
to verify that the active SDK is now .NET 5.0, and then I run the same command to create the Web API project with the namedotnet new webapi -o dummyapi50
. I then editWeatherForecastController
in exactly the same way and then executedotnet run
to get the following output:C:\Users\Habib\source\repos\Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features\code\Chapter01\dummyapi50>dotnet run Building...info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Now listening on: https://localhost:5001 info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Application started. Press Ctrl+C to shut down.info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Hosting environment: Development info: Microsoft.Hosting.Lifetime[0]Â Â Â Â Â Â Content root path:C:\Users\Habib\source\repos\Adopting-.NET-5--Architecture-Migration-Best-Practices-and-New-Features\code\Chapter01\dummyapi50
Now that we've set up the benchmark correctly, let's see how to execute it.
Executing the benchmark
Since our Web API projects are now set up, we are going to use the simple benchmarking application named Apache Bench. This is free and open source and the binary is available from within the Apache installer package:
- I use Windows, so I downloaded the ZIP file installation via the following link:
http://httpd.apache.org/docs/current/platform/windows.html#down
. This downloaded thehttpd-2.4.46-o111h-x86-vc15.zip
file to my machine and, from this ZIP file, I just extracted one file,ab.exe
, which is all I need. - I use the following configuration with Apache Bench to send the requests to our Web API project with 10 concurrent threads and a total of 50,000 requests:
ab -n 50000 -c 10 http://localhost:5000/WeatherForecast
- Now, executing this against our
dummyapi31
project, which is .NET Core 3.1-based, generates the following output:
And executing the same ab
command against our dummyapi50
project, which is .NET 5.0-based, generates the following output:
Note that I run the Apache Bench for each .NET version 5 times with no gaps, or say with a gap of a maximum of 1 second in between the calls, and obtained the statistics from the fifth run, meaning I ran 50,000 requests 5 times and got the results just from the fifth run. This was done in order to also see whether there is some kind of caching that is applied as well as a GC function that might happen in between as well.
From the results, we can definitely see the result that .NET 5.0 is faster than .NET Core 3.1. Even though the improvement is marginal, 0.2 to 0.5% faster, this tiny Web API is not meant to indicate a huge difference, but for a sizeable project, it demonstrates the capabilities in a very simple way that you can benchmark your own project against the two runtimes almost without modifying the code and see the performance gains by yourself in the real application.
Here, we can see that although the difference in performance is small, .NET 5 still performs faster in a tiny application with the very few APIs and features used.
Ending with these two benchmarking examples, we finalize the .NET performance topic. We have observed the performance improvements from various .NET versions down to the latest .NET 5 and noticed how .NET 5 encompasses all the improvements achieved by the previous .NET versions already baked into it.
The fact that the use of .NET 5 automatically improves the performance of the application without the need to change the code ought to be appreciated. For example, if the application was built using .NET Core 3.1 and is now compiled using .NET 5, it receives an additional performance boost.
The version of Kestrel that is included with .NET 5 already exhibits a significant performance improvement in terms of serving the APIs. Therefore, the same application code that was previously compiled with the older .NET version and is now compiled with .NET 5 and served by Kestrel does automatically get the better performance.
Hence, the effort of migrating the application to .NET 5 automatically adds a performance boost as an additional benefit without having to change the code with the purpose of improving performance.
Now that we have covered performance improvements and understood how it works in different .NET versions, let's look at the release schedule for .NET.