Performance issues when using a method as a parameter in C#. Are they real?
#csharp #benchmarkThere is an article on Habr about performance issues when passing a method as a parameter in C#. The author showed that passing an instance method as a parameter inside for
loops may degrade performance and increase memory consumption due to unnecessary object allocations in the heap. In this short article, I want to repeat the original benchmark and compare how things have changed since the .NET 7 release.
Benchmark
In the original article, the author compared only two possible declarations of a method: a predefined delegate and an instance method. In addition to that, I decided to check other ways of method declaration. So, the final list is as follows:
- predefined delegate;
- lambda expression;
- lambda expression that calls instance method;
- lambda expression that calls static method;
- static anonymous method;
- static anonymous method that calls static method;
- anonymous method;
- anonymous method that calls instance method;
- anonymous method that calls static method;
- instance method;
- static method.
For benchmarking, I used the BenchmarkDotNet library. The whole code of the benchmark class can be found here.
Results
The benchmark results are in the diagram below. As we can see, .NET 7 shows better results for everything related with static methods (except anonymous method + static method).
The benchmark results
To understand why this happens, we have to go deeper and look at IL code. Basically, all these possible ways of method calling can be divided into two groups:
- Which causes creation of a new object in the
for
loop on each iteration. - Which doesn’t cause creation of a new object in a
for
loop on each iteration
For example, let’s compare the code for calling a static method (4th row in the diagram):
for (int i = 0; i < n; i++) {
CallAdd(StaticAdd, i, i);
}
In .NET 6, such code after compilation will look like:
for (int i = 0; i < 10000; i++) {
// A new Func instance created on each iteration.
CallAdd(new Func<int, int, int>(
(object) null,
__methodptr(StaticAdd)
), i, i);
}
In .NET 7 the situation is different:
for (int i = 0; i < 10000; ++i) {
CallAdd(
// A new Func instance created only once
BenchmarkableClass.<>O.<1>__StaticAdd ?? (
BenchmarkableClass.<>O.<1>__StaticAdd = new Func<int, int, int>(
(object) null,
__methodptr(StaticAdd)
)), i, i
);
}
The compiler created a new hidden static class <>O
with public field <1>__StaticAdd
of type Func<int, int, int>
and this field will be initialized only once at the first iteration. The same situation and for other cases with static method.
Suboptimal .NET 6 code that creates new objects on each iteration causes unnecessary heap allocations. Therefore, it also causes garbage collection, which also affects performance.
Conclusion
Answering to the question in the title of this article: yes, the performance issues are still real, but the .NET platform becomes better.
If you’re using .NET 6 for development, it’s better to predefine a delegate instance before for
loop or use lambda expression with static method.
For .NET 7 there are more options. You still can use predefined delegate or lambda expressions with static methods. But you can also pass a static method directly or pass an anonymous static method.