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:
1 2 3 4 5 6 7 8 9 10 11 12 |
int sum = Sum(1, 2, 3, 4, 5); Console.WriteLine(sum); sum = Sum(1, 2, 3, 4, 5, 6 ,7); Console.WriteLine(sum); static int Sum(params int[] numbers) { int result = 0; for(int i = 0; i < numbers.Length; i++) { result += numbers[i]; } return result; } |
If we decompile this program with ILSpy, we can see the array allocations:
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.
1 2 3 4 5 6 7 8 9 10 11 12 |
int sum = Sum(1, 2, 3, 4, 5); Console.WriteLine(sum); sum = Sum(1, 2, 3, 4, 5, 6 ,7 ,8); Console.WriteLine(sum); static int Sum(params ReadOnlySpan<int> numbers) { int result = 0; for(int i = 0; i < numbers.Length; i++) { result += numbers[i]; } return result; } |
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:
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)
:
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
You might want to read these articles if ref struct
, ref
fields and Span<T>
benefits are not clear for you.
- Managed pointers, Span, ref struct, C#11 ref fields and the scoped keyword
- Improve C# code performance with Span<T>
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.
.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)
.
1 2 3 |
from m in Application.Methods where m.WasAdded() && m.Name.Contains("ReadOnlySpan") select m |
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:
1 |
result = string.Concat("1", "2", "3", "4", "5", "6", "7", "8", "9"); |
…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:
1 2 3 4 5 6 7 8 9 10 11 12 |
int sum = Sum([1, 2, 3, 4, 5]); Console.WriteLine(sum); sum = Sum([1, 2, 3, 4, 5, 6 ,7 ,8]); Console.WriteLine(sum); static int Sum(ReadOnlySpan<int> numbers) { int result = 0; for(int i = 0; i < numbers.Length; i++) { result += numbers[i]; } return result; } |
Conclusion
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.