Делегаты и производительность

107
30 января 2021, 21:40

Делегат это ссылка на метод. То есть если мы делаем элементарную реализацию типа такой:

public delegate void del1(string message);
...
{
del1 objdel=new del1(ShowMessage);
}
public static void ShowMessage(string message)
{
}

Это просто вызов функции. Но например если мы делаем анонимный метод типа такого:

public delegate void del1(string message);
...
{
del1 objdel=delegate(string message){
...
};
}

То в этом случае, ради такой конструкции будет создаваться отдельный класс, и в нем будет создаваться метод для выполнения? И поэтому если часто использовать такие методы то это скажется негативно на производительности, так?

Answer 1

Когда ты объявляешь тип делегата, компилятор генерирует код класса-делегата (который наследуется от MulticastDelegate) - это и есть его сущность. Благодаря статической типизации компилятор может еще на этапе компиляции проверить соответствие сигнатуры метода, который ты передал в конструктор класса делегата (new Action(method), например), самому делегату. Я не знаю как на низком уровне работают делегаты, но однозначно перед непосредственным выполнением обернутого в делегат метода существует несколько дополнительных операций: Action act; act() или act.Invoke() - одно и то же. act() компилятором преобразуется в act.Invoke() - своего рода синтаксический сахар. Так вот о накладных операциях: в стеке появится вызов Invoke -> возможно еще чего-то -> самого метода. В последнем абзаце я думаю ты имел ввиду это:

Вот такой код:


using System;
namespace tester
{
    class Program
    {
        static void Main(string[] args)
        {
            var act = new Action(() => Console.Write(true));
        }
    }
}

Скомпилируется в -


// tester.Program
using System;
using System.Runtime.CompilerServices;
using tester;
internal class Program
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();
        public static Action <>9__0_0;
        internal void <Main>b__0_0()
        {
            Console.Write(true);
        }
    }
    private static void Main(string[] args)
    {
       //Здесь проверяется создан ли экземпляр делегата, и его создание в противном случае.   
       //Таким образом, можно говорить, что создание делегата для анонимной ф-ции выполняется один раз за работу программы (если нет захвата внешних переменных).
        Action act = <>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Action(<>c.<>9.<Main>b__0_0));
    }
}

Стоит сказать, что разницы нет, что метод в другом классе. Такой код чище выходит:


    class Program
        {
            static void Print() => Console.Write(true);
            static void Main(string[] args)
            {
                var act = new Action(Print);
            }
        }

Декомпиляция:


    // tester.Program
    using System;
    internal class Program
    {
        private static void Print()
        {
            Console.Write(true);
        }
        private static void Main(string[] args)
        {
            Action act = Print;
        }
    }

И посмотрим на его IL:

А в этом случае при каждом вызове метода заново создается экземпляр делегата. В предыдущем, напомню, у нас каждый раз при вызове метода проверяется создавали ли мы уже экземпляр (причем поля-то были статичные). Что дороже решайте сами (я так думаю проверка легковесней). Но оптимизировать код так: экономия на спичках. И еще один момент, зачем все-таки создается класс под анонимную ф-цию? Для захвата внешних переменных. Пример:


    using System;
    namespace tester
    {
        class Program
        {
            static void Main(string[] args)
            {
                int count = 1;
                var del = new Action(delegate () {
                    Console.WriteLine(count);
                });
            }
        }
    }

Сигнатура Action - безпараметричная - ф-ция. Но фактически внутри идет вывод значения count, то есть передача параметра. Для этого и создается обертка. Видим:


    // tester.Program
    using System;
    using System.Runtime.CompilerServices;
    using tester;
    internal class Program
    {
        [CompilerGenerated]
        private sealed class <>c__DisplayClass0_0
        {
            public int count;
            internal void <Main>b__0()
            {
                Console.WriteLine(count);
            }
        }
        private static void Main(string[] args)
        {
            //Создается класс-обертка, которая захватит внешние переменные метода.
            <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
           //Захват переменной. Тут даже оптимизация (count - не переменная метода, а экземпляра оболочки для захвата)
            <>c__DisplayClass0_.count = 1;
            //Создание экземпляра.
            Action del = new Action(<>c__DisplayClass0_.<Main>b__0);
        }
    }

Кстати, когда используется экземплярный метод для оборачивания делегатом, тогда происходит захват и самой сущности (экземпляра то есть). Вот сигнатура конструктора Action: Первый аргумент: ссылка this для экземплярного метода или null, если метод неэкземплярный. А второй - IntPtr-ссылка на метод. Вот кусочек из первого примера: Что можно сказать на последок... Вызов делегата - это не просто вызов ф-ции. Автоматический захват внешних переменных - классно и удобно. Раньше еще использовали анонимные ф-ции не только для передачи куда-то (например, для обработчика события), а еще в роли локальных ф-ций. Удобно это потому, что анонимные ф-ции позволяли выполнять захват внешних переменных и не приходилось писать огромную тучу аргументов для вызова. То есть:


    using System;
    namespace tester
    {
        class Program
        {
            static void Main(string[] args)
            {
                int count = 1;
                var inc = new Action(() => count += 15);
                inc();
                inc();
            }
        }
    }

Вместо того, чтобы сделать ф-цию void inc(int count) - т.к. это более громоздко. Но недавно в C# ввели локальные ф-ции и теперь это работает более оптимально, нежели когда это реализовывалось посредством делегатов:


    using System;
    namespace tester
    {
        class Program
        {
            static void Main(string[] args)
            {
                int count = 1;
                void printCount() => Console.WriteLine(count);
                printCount();
            }
        }
    }

Скомпилируется в -


    // tester.Program
    using System;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using tester;
    internal class Program
    {
        [StructLayout(LayoutKind.Auto)]
        [CompilerGenerated]
        private struct <>c__DisplayClass0_0
        {
            public int count;
        }
        private static void Main(string[] args)
        {
            <>c__DisplayClass0_0 <>c__DisplayClass0_ = default(<>c__DisplayClass0_0);
            <>c__DisplayClass0_.count = 1;
            <Main>g__printCount|0_0(ref <>c__DisplayClass0_);
        }
        [CompilerGenerated]
        internal static void <Main>g__printCount|0_0(ref <>c__DisplayClass0_0 P_0)
        {
            Console.WriteLine(P_0.count);
        }
    }

Вот тут действительно просто вызов ф-ции + структура для захвата внешних аргументов. И отвечая на твой финальный вопрос: нет. Эти 2 (с использованием анонимок и оборачиванием ф-ции класса) варианта особо не уступают друг другу в производительности. Мне еще нравится юзать локальные ф-ции для оборачивания в делегаты - более стройный код получается. Интересно во что это компилится. Но проверять не буду.

Answer 2

Делегат - это также специальный класс, унаследованный от MulticastDelegate.

Вообще, чтобы присвоить анонимный метод чему-то, надо, чтобы это что-то имело явно определённый тип:

using System;
namespace CSrharpApplicationTest
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Action x = delegate () // С var не сработает.
            {
            };
        }
    }
}

Так происходит потому что может возникнуть неоднозначность, как в следующем случае:

namespace CSrharpApplicationTest
{
    internal class Program
    {
        public delegate void A();
        public delegate void B();
        private static void Main(string[] args)
        {
            var x = delegate () // Что выбрать в качестве типа x - A или B? Компилятору не ясно.
            {
            };
        }
    }
}

Хочется заметить, что может возникнуть вопрос такого рода: Почему в местах где нет неоднозначности такое не разрешили? Я думаю, что так поступили для большей стандартизации кода и облегчения его написания. Ибо если бы разрешили такое поведение с var, то при добавлении ссылки на пространство имён, содержащем делегат с похожей сигнатурой могла возникнуть неоднозначность, что означало бы дополнительное время на правку кода.

Возвращаясь к первому примеру, в нём я создаю неявно экземпляр делегата Action со ссылкой на метод, содержимого которого заключено в {}. То есть, здесь (в контексте этого примера) всё происходит ровно также как и без использования анонимных методов:

using System;
namespace CSrharpApplicationTest
{
    internal class Program
    {
        public static void Method()
        {
        }
        private static void Main(string[] args)
        {
            Action x = Method;
        }
    }
}

Так что с точки производительности различий не будет. Следует понимать, что делегаты - это более безопасное средство для ссылок на методы, нежели указатели в C++, за что приходится платить производительностью в некоторой степени.

Обращаю внимание на то, что обычно указывают тип переменной явно, а не используют var.

READ ALSO
Бот VK, параметр указан не верно или пропущен

Бот VK, параметр указан не верно или пропущен

Пишу бота для ВК, когда запрашиваю информацию об 1 человеке из группы, всё работает - получаю имя и фамилиюЕсли запрашиваю информацию сразу...

100
MS SQL где запущен сервер?

MS SQL где запущен сервер?

Во вкладках Visual Studio нашел вкладку, где показывает, что у меня на компьютере установлен сервер MS SQL, хотя я его не устанавливал:

100
Усекаются ли данные при превышении их размера при использовании типа CHAR?

Усекаются ли данные при превышении их размера при использовании типа CHAR?

В книге про тип данных VARCHAR сказано, что если присвоить строковое значение длиннее позволенного, то оно будет усечено до максимальной длины,...

104