Laszlo

Hello, I am Laszlo

Software-Engineer, .NET developer

Contact Me

Deep dive into .NET 11 Performance Improvements

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:

  1. Current post: Allocation free numbers
  2. Virtual generic method calls
  3. Vector zero equality
  4. Guid creation on Linux
  5. JIT generating implicit-else assembly instructions
  6. Guid equality
  7. Copy-on-write locking strategy

Allocation free numbers

One of the early examples in the Deabstraction section discusses how the JIT avoids heap allocations through improved escape analysis.

[SimpleJob]
[DisassemblyDiagnoser]
public class Benchmarks
{
    private int? _nullableValue = 10;

    [Benchmark]
    public string? FormatNullableInt() => Format(_nullableValue);

    private static string? Format<T>(T value)
    {
        if (value is IFormattable formattable)
            return formattable.ToString(null, null);

        return null;
    }
}

When the Format method executes the type check, it has to box the T value. However, the JIT sees that the formattable reference does not escape from the Format method, so it can avoid allocating it on the heap. This results in net-zero heap allocations.

But how does this benchmark result in zero heap allocations when it still has to create a string that is allocated on the heap?

The trick is hidden in the actual IFormattable implementation, which caches numbers smaller than 300.

This can be validated by changing the _nullableValue field to 300, which produces a heap allocation:

| Method            | Mean     | Code Size | Allocated |
|------------------ |---------:|----------:|----------:|
| FormatNullableInt | 15.15 ns |     481 B |      32 B |

This caching is, of course, limited by several factors: format string, value, negative sign, input type, and so on.

Creating a string still results in an allocation, but when the value falls within a cacheable range, the value is cached for the lifetime of the application. That makes the overall allocation profile effectively amortized to zero in the original example.

Conclusion

This post focused on the first of many interesting nuances I learned from the Performance Improvements in .NET 11 article. It explored how a string return type may result in zero heap allocations. In the next post, I’ll look at the optimized assembly code for generic virtual methods.