C# Производительность лямбда-выражений
Правда ли, что с точки зрения производительности лучше использовать статический метод вместо лямбда-выражения? Так как лямбда-выражение каждый раз будет порождать объект замыкания (closure) и сохранять ссылки на используемые локальные переменные.
Ответы (2 шт):
Неправда. Если лямбда не использует замыкание, то она именно в статический метод и превратится. А если использует, то фиг ты её на статический метод заменишь.
Лямда тоже может быть статической, например:
SynchronizationContext.Current.Post(static _ => Console.WriteLine(), null);
Лямбда компилируется в метод, в отдельном классе, это несовсем замыкание. А вот чтобы достичнь оптимальной легковесности и производительности, захватываемых аргументов в лямбде стоит избегать.
Давайте сравним, что покажет декомпиляция IL.
Вот с захватываемым аргументом
public class C
{
public void M()
{
string hello = "Hello, world!";
SynchronizationContext.Current.Post(_ => Console.WriteLine(hello), null);
}
}
Получается так
public class C
{
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
public string hello;
[NullableContext(2)]
internal void <M>b__0(object _)
{
Console.WriteLine(hello);
}
}
public void M()
{
<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
<>c__DisplayClass0_.hello = "Hello, world!";
SynchronizationContext.Current.Post(new SendOrPostCallback(<>c__DisplayClass0_.<M>b__0), null);
}
}
Получился тот самый класс.
А вот без захватываемого аргумента
public class C
{
public void M()
{
string hello = "Hello, world!";
SynchronizationContext.Current.Post(h => Console.WriteLine(h), hello);
}
}
public class C
{
[Serializable]
[CompilerGenerated]
private sealed class <>c
{
public static readonly <>c <>9 = new <>c();
public static SendOrPostCallback <>9__0_0;
[NullableContext(2)]
internal void <M>b__0_0(object h)
{
Console.WriteLine(h);
}
}
public void M()
{
string state = "Hello, world!";
SynchronizationContext.Current.Post(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new SendOrPostCallback(<>c.<>9.<M>b__0_0)), state);
}
}
Что же произошло? Правильно, лямда стала статичской, даже если я явно не написал ей, что она static
. Компилятор умный, сам догадался. А если быть точнее, то здесь не совсем статика, а синглтон. Но в любом случае, это уже значит, что лишних аллокаций не будет.
Поэтому обращайте внимание на наличие возможности выполнить такого рода оптимизацию, то есть пробросить данные через реальные аргументы, а не через захватываемые.
А разница в производительности в том, что когда лямда не статическая, то уходит немного времени на создание экземпляра класса, где она живёт, и передачу ему аргументов.
Кстати, а вот что будет, если вообще убрать лямбду, как советует IDE:
public class C
{
public void M()
{
string hello = "Hello, world!";
SynchronizationContext.Current.Post(Console.WriteLine, hello);
}
}
Барабанная дробь...
public class C
{
[CompilerGenerated]
private static class <>O
{
public static SendOrPostCallback <0>__WriteLine;
}
public void M()
{
string state = "Hello, world!";
SynchronizationContext.Current.Post(<>O.<0>__WriteLine ?? (<>O.<0>__WriteLine = new SendOrPostCallback(Console.WriteLine)), state);
}
}
Синглтон исчез, и имеем теперь просто статический делегат.
Ещё любая лямбда сама по себе подразумевает заворачивание вызова в делегат, поэтому да, если есть возможность написать код в обычном методе - пишите и вызывайте напрямую, тогда можно будет вовсе без делегатов обойтись.
Что касается разницы в производительности между статическим методом и методом текущего экземпляра, то последний капельку быстрее. Так во всяком случае показывали бенчмарки, которые я когда-то гонял, решая очень требовательные к производительности задачи.
Кстати, показанный выше декомпилированный C# код даёт мало представления о производительности. Чтобы получить такое представление, нужно сравнивать сгенеренный JIT-компилятором ассемблер и гонять бенчмарки. Бенчмарк покажет, что быстрее, а ассемблер - почему бысстрее.