NDepend Blog

Improve your .NET code quality with NDepend

C# 13 params collections

June 19, 2024 3 minutes read

Here is a quick post to explain how the upcoming C# 13 params collections feature will free 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.

.NET 9 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-preview.4.24266.19. 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

My hope, is that just recompiling your code with C# 13 and .NET 9 will free many allocations without requiring any code changes.

When compiling against .NET 9 preview 2.0 (in Visual Studio 2022 17.11.0 Preview 2.0) this expression:

…the compiler still binds to string Concat(params string?[] values). However, by looking at the source code of this preview we can see /*params*/ ReadOnlySpan<string?>.

Source Code net9.0 preview

I guess it will be fixed soon since one of the motivations for C# 13 params collections is: Another motivation is ability to add a params span overload and have it take precedence over the array version, just by recompiling existing source code.

Related to the remark above, in  this preview version I cannot have both methods int Sum(params int[] numbers) and int Sum(params ReadOnlySpan<int> numbers).

params overload

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.