Deep dive into .NET 11 Performance Improvements #211/05/2026 | 3 minutes to read | Open in Markdown
In this series of posts, I am going to explore some of the most interesting .NET 11 performance improvements I learned about in Stephen Toub's blog post, Performance Improvements in .NET 11.
Each of my post focuses on one example from the original article, but digs a little deeper into one additional aspect that was not covered there. The series covers the following topics:
- Allocation free numbers
- Current post: Virtual generic method calls
- Vector zero equality
- Guid creation on Linux
- JIT generating implicit-else assembly instructions
- Guid equality
- Copy-on-write locking strategy
Virtual generic method calls
One of the early examples in the Deabstraction section discusses how JIT code generation optimizes virtual dispatch for generic methods.
public class Benchmarks { [Benchmark] public int NonShared() => ((IProcessor)new Processor()).SizeOf(new object()); private interface IProcessor { int SizeOf<T>(T value); } private sealed class Processor : IProcessor { public int SizeOf<T>(T value) => Unsafe.SizeOf<T>(); } }
The original post details the shared and non-shared cases, referring to whether the generic type parameter is a reference type (shared, or 'canon') or a value type (non-shared). It explains that the JIT can turn the interface call into a devirtualized method call on the actual Processor type, because argument setup is moved to a later point in the analyis. This enables further optimizations: in this example, the Processor object can be stack allocated, and the SizeOf method can be inlined.
It is interesting to see how the JITted assembly code changes from .NET 10 to .NET 11. In .NET 10, the JITted code is 291 B and includes two heap allocations: new Processor and new object().
In .NET 11, after all optimizations, the code is 6 B with no heap allocations. The method is folded and inlined into a move that places the value 8 (the size of a reference type on x64) into the result register:
; Benchmarks.NonShared()
mov eax,8
ret
; Total bytes of code 6
In practice, .NET 11 can completely skip the System.Runtime.CompilerServices.VirtualDispatchHelpers.VirtualFunctionPointer function call when possible.
Conclusion
This post focused on the second of many interesting nuances I learned from the Performance Improvements in .NET 11 article. It explored how the .NET 11 JIT optimizes virtual generic method calls through devirtualization, inlining, and stack allocation. Compared with .NET 10, the example's generated code shrinks from 291 bytes with two heap allocations to just 6 bytes with no allocations.