There 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).

content 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:

  1. Which causes creation of a new object in the for loop on each iteration.
  2. 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.