NDepend Blog

Improve your .NET code quality with NDepend

C# 13 params collections

December 3, 2024 3 minutes read

Here is a quick post to explain how the upcoming C# 13 params collections feature frees your code from many allocations.

C# params prior C# 13

Until now, using the params keyword implicitly allocated a new array object at runtime for each call (unless you explicitly allocated and passed the array yourself). This is demonstrated by the program below:

If we decompile this program with ILSpy, we can see the array allocations:C# params array allocation

C# 13 params ReadOnlySpan<T>

Now let’s update this program with C# 13 with int Sum(params ReadOnlySpan<int> numbers). This is a new C# 13 possibility, until now only the keyword params followed by an array was allowed.

If we decompile this C# 13 program in IL we can see that the parameter lists of the calls to Sum(...) are defined as fields declared in a generated class named <PrivateImplementationDetails>. These fields are typed with generated structures nested in the same class:

C# 13 params struct array allocation

Interestingly, we need to decompile to C# to see the [StructLayoutAttribute] that adjusts the footprint of the generated structures to:

  • 5 x 4 = 20 bytes for Sum(1, 2, 3, 4, 5)
  • 5 x 8 = 32 bytes for Sum(1, 2, 3, 4, 5, 6, 7, 8) :

C# 13 params struct array allocation 2

Now let’s decompile the main method. Unsurprisingly, the calls to the method Sum(...) use the generated fields. The method RuntimeHelpers.CreatedSpan<int32>() obtains a Span<int> instance from each of these fields. The beauty of Span<T> is that it resides on the stack because it is a ref struct. Also, each span contains a ref field that points toward the memory. As a consequence, there is zero heap allocation in this C# 13 version. This improves performance because the Garbage Collector has fewer objects to manage C# 13 params struct array allocation call site

You might want to read these articles if ref struct, ref fields and Span<T> benefits are not clear for you.

C# 13 params methods overloading

C# 13 lets you have different overloaded methods with params. At call site, the compiler is then smart enough to resolve to the cheapest overload. This is illustrated by the screenshot below that shows that expensive overloads are not resolved by the compiler.

C# 13 params overload binding

.NET 9 Base Class Library updates with params ReadOnlySpan<T>

Using NDepend, we compared the assemblies in the folder C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.5 with those in C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0. With this CQLinq code query, we matched 606 new methods in .NET 9 Base Class Library that utilize ReadOnlySpan<T> parameters. Most of these methods are counterparts of params T[] versions like string Concat (params string?[] values).

.NET 9 new params methods

Just recompiling your code with C# 13 and .NET 9 will free many allocations without requiring any code changes.

When compiling against .NET 9.0.0 this expression:

…the compiler create an inline array of length 9 to call this overload string Concat(params ReadOnlySpan<string> values). This way no array gets allocated on the heap.

C# 12 Collections Expressions

With C# 12 Collection Expressions, you can achieve a quite similar syntax without any heap allocations:

Conclusion

Since the inception of C#, I have avoided using the params keyword whenever possible. Allocating an array just to pass an indefinite number of parameters to a method is a too high price to pay. Thankfully, C# 13 addresses this issue. To take advantage of this new optimization for calls to .NET Framework methods declared with params, hopefully, you will simply need to recompile your code with C# 13 and .NET 9.

Leave a Reply

Your email address will not be published. Required fields are marked *