Опубликовано
- 2 мин чтения
Проблемы с производительностью при передаче метода в C#

На Хабре есть статья о проблемах производительности при передаче метода в качестве параметра в C#. Автор показал, что передача экземплярного метода в цикле for
может ухудшать производительность и увеличивать расход памяти из-за лишних аллокаций в куче. В этой короткой заметке я хочу повторить оригинальный бенчмарк и сравнить, что изменилось после выхода .NET 7.
Бенчмарк
В оригинальной статье автор сравнивал только два варианта объявления метода: заранее заданный делегат и экземплярный метод. Я решил проверить и другие способы. Вот полный список:
- заранее заданный делегат;
- лямбда-выражение;
- лямбда-выражение, вызывающее экземплярный метод;
- лямбда-выражение, вызывающее статический метод;
- статический анонимный метод;
- статический анонимный метод, вызывающий статический метод;
- анонимный метод;
- анонимный метод, вызывающий экземплярный метод;
- анонимный метод, вызывающий статический метод;
- экземплярный метод;
- статический метод.
Для тестирования я использовал библиотеку BenchmarkDotNet. Полный код тестов доступен здесь.
Результаты
Результаты бенчмарка на диаграмме ниже. Как видно, в .NET 7 лучше работают все варианты со статическими методами (кроме анонимного метода, вызывающего статический).

Результаты бенчмарка
Чтобы понять, почему так происходит, нужно заглянуть в IL-код. Все способы передачи метода можно разделить на две группы:
- Которые создают новый объект на каждой итерации цикла
for
. - Которые не создают новый объект на каждой итерации.
Например, рассмотрим вызов статического метода (4-я строка на диаграмме):
for (int i = 0; i < n; i++) {
CallAdd(StaticAdd, i, i);
}
В .NET 6 после компиляции это будет выглядеть так:
for (int i = 0; i < 10000; i++) {
// На каждой итерации создаётся новый экземпляр Func
CallAdd(new Func<int, int, int>(
(object) null,
__methodptr(StaticAdd)
), i, i);
}
А в .NET 7 ситуация другая:
for (int i = 0; i < 10000; ++i) {
CallAdd(
// Новый экземпляр Func создаётся только один раз
BenchmarkableClass.<>O.<1>__StaticAdd ?? (
BenchmarkableClass.<>O.<1>__StaticAdd = new Func<int, int, int>(
(object) null,
__methodptr(StaticAdd)
)), i, i
);
}
Компилятор создаёт скрытый статический класс <>O
с публичным полем <1>__StaticAdd
типа Func<int, int, int>
, которое инициализируется только один раз при первой итерации. То же самое происходит и для других вариантов со статическими методами.
Неоптимальный код .NET 6, который создаёт новые объекты на каждой итерации, вызывает лишние аллокации в куче. Из-за этого дополнительно запускается сборка мусора, что тоже влияет на производительность.
Выводы
Да, проблемы с производительностью всё ещё существуют, но платформа .NET стала лучше.
Если вы разрабатываете на .NET 6, лучше заранее создавать экземпляр делегата перед циклом for
или использовать лямбда-выражение со статическим методом.
В .NET 7 вариантов больше. Можно использовать заранее созданный делегат, лямбда-выражение со статическим методом, напрямую передавать статический метод или использовать статический анонимный метод.