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:
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>
.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)
.
1 2 3 |
from m in Application.Methods where m.WasAdded() && m.Name.Contains("ReadOnlySpan") select m |
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:
1 |
result = string.Concat("1", "2", "3", "4", "5", "6", "7", "8", "9"); |
…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?>
.
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)
.
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.