C# Производительность лямбда-выражений

Правда ли, что с точки зрения производительности лучше использовать статический метод вместо лямбда-выражения? Так как лямбда-выражение каждый раз будет порождать объект замыкания (closure) и сохранять ссылки на используемые локальные переменные.


Ответы (2 шт):

Автор решения: Qwertiy

Неправда. Если лямбда не использует замыкание, то она именно в статический метод и превратится. А если использует, то фиг ты её на статический метод заменишь.

→ Ссылка
Автор решения: aepot

Лямда тоже может быть статической, например:

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-компилятором ассемблер и гонять бенчмарки. Бенчмарк покажет, что быстрее, а ассемблер - почему бысстрее.

→ Ссылка