This article is more than 1 year old
Faster .NET? Monster post by Microsoft software engineer shows serious improvements
Essential reading for developers in search of performance and efficiency
The forthcoming .NET 6 will be significantly faster than its predecessors, according to a monster post by Microsoft Partner Software Engineer Stephen Toub.
Toub's 45,000-word post follows similar ones for earlier versions since .NET Core 2.0, and is based on an analysis of all merged pull requests (PRs – changes to the code) that were likely to affect performance.
Around 400 PRs are covered, out of 550 candidates, compared to 250 for .NET 5. These posts are always worth investigating for .NET developers since they include performance tips as well as a deep dive into the inner workings of Microsoft's cross-platform runtime.
There is no single answer to the question "how much faster is .NET 6.0?" (and there may be cases where it is slower). What Toub supplies is benchmark code that compares both performance and the size of the generated code from .NET 6.0, currently in late preview, with .NET Framework 4.8 (the final version of old-style Windows-only .NET) and .NET 5.0 (the current release).
The .NET 6.0 runtime is particularly significant since it is a long-term support release. Looking at the benchmarks Toub posts for each case, the results vary though around 25 per cent is common.
Significant speed-up in .NET 6.0. In this example, .NET 6.0 is around 25% faster than older versions, but generates slightly larger code
In certain cases, .NET 6.0 reduces the time taken to almost nothing. An example is the new Environment.ProcessPath API for getting the path of the current process, which is 72,000 times faster and generates no code, compared to the previous approach using Process.GetCurrentProcess().MainModule.FileName. Similarly, there is an entertaining case where the new optimizer spots that a function in the code always has the same inputs, which are defined as constants, and is therefore able to replace the entire function with a constant, reducing both the time taken and the size to almost zero.
This latter case uses a technique called dynamic profile-guided optimization (PGO) which is new in .NET 6.0. The idea is that the compiler does a first compile with instrumentation and then uses the results to optimize a second compilation.
Note that Toub said: "In .NET 6, dynamic PGO is off by default. To enable it, you need to set the DOTNET_TieredPGO environment variable."
Toub also said that the Filestream code, used for reading and writing files, has been rewritten in .NET 6.0. "FileStream has also been plagued over the years by numerous performance-related issues, most of which are part of its asynchronous I/O implementation on Windows," he said, claiming that in the new version "all of these issues are resolved."
Another change is that over 2,300 "internal and private" classes in the .NET 6.0 code have been newly marked as sealed. A sealed class cannot be inherited, which means the compiler can deduce that a virtual method (which if inherited can be overridden with different code) is not in fact overridden and therefore there is no need to look for this different code.
In networking, data sent via websockets can now be compressed per-message. This is a trade-off since the compression takes more processing time, but the smaller message is faster to transmit. "That could be a good tradeoff if communicating over a real network with longer latencies," said Toub.
- Microsoft emits last preview of .NET 6 and C# 10, but is C# becoming as complex as C++?
- Stack Overflow survey: Microsoft IDEs dominate, GCP and Azure battle behind AWS
A recent framework called Blazor means that .NET code can run in a web browser as WebAssembly. Microsoft has put a lot of effort into optimizing this for .NET 6.0.
With Blazor, "an entire .NET application, inclusive of the runtime, is downloaded to and executed in a browser," Toub explained. The size of a Hello World Blazor application in .NET 5.0 is around 2.10MB. In .NET 6.0 this falls to 1.88MB, for reasons which he detailed in the post. Using additional tools based on Emscripten the size falls to 1.82MB.
At this point Toub adds a handy hint: there is a ton of code in .NET's Globalization routines for formatting output for different languages and cultures. If this is not needed, there is a switch developers can set which further reduces the size, to just 1.07MB. Unfortunately, this is for Hello World and a real-world Blazor application will likely be much larger, but this is still an impressive result.
Toub also noted that the Blazor runtime is based on Mono. "There are actually two runtime implementations in dotnet/runtime: coreclr and mono... Blazor WebAssembly relies on mono, which has been honed over the years to be small and agile for these kinds of scenarios, and has also received a lot of performance investment in .NET 6."
Other performance techniques include smarter code inlining (copying code to where it is called), optimizing for value types such as structs, loop alignment which moves compiled code so the CPU can fetch it more efficiently, and better performance for code which checks whether a type implements an interface, and a new faster implementation of the algorithm for pseudo-random numbers (though the old algorithm is used for compatibility reasons if a seed is supplied).
Impressive, but why is there not a native code compiler for .NET that can compile single-file executables? The answer is in this post about .NET Runtime Form Factors in which the .NET Team explained:
At one point, the goal of .NET Native and CoreRT projects was to replace the established .NET runtime implementation in its entirety. We even had a project for that called Rover – 'Runtime over RedHawk'. This goal was proven to be unrealistic. Re-architecting half of the .NET features built over 20 years (with a large team) to run on the nice clean runtime is prohibitively expensive. Executing this endeavor would require slowing down the investment into the mainstream .NET runtime to a trickle. The vast majority of customers would not see any material improvements for number of years. We consider that direction unacceptable.
Microsoft chose instead to bring over smaller improvements piece by piece to the .NET Runtime so that all .NET code benefits – though there is still an experimental NativeAOT project for those who consider the trade-off worth pursuing. ®