Производительность вызова делегатов vs методы

после этого вопрос - передать метод в качестве параметра с помощью C# и некоторые из моего личного опыта я хотел бы знать немного больше о производительности вызова делегата, против просто вызов метода в C#.

хотя делегаты очень удобны, у меня было приложение, которое сделало много обратных вызовов через делегатов, и когда мы переписали это, чтобы использовать интерфейсы обратного вызова, мы получили улучшение скорости на порядок. Это было с .NET 2.0, поэтому я не уверен, как все изменились с 3 и 4.

как вызовы делегатов обрабатываются внутренне в компиляторе / CLR и как это влияет на производительность вызовов методов?


редактировать - чтобы уточнить, что я имею в виду под делегатами против интерфейсов обратного вызова.

для асинхронных вызовов мой класс может предоставить событие OnComplete и связанный делегат, на который вызывающий абонент может подписаться.

кроме того, я могу создать интерфейс ICallback с Метод OnComplete, который реализует вызывающий объект, а затем регистрирует себя с классом, который затем вызовет этот метод по завершении (т. е. способ Java обрабатывает эти вещи).

5 ответов


Я не видел этого эффекта - я, конечно, никогда не сталкивался с тем, что это узкое место.

вот очень грубый и готовый тест, который показывает (на моем поле в любом случае) делегаты на самом деле быстрее чем интерфейсы:

using System;
using System.Diagnostics;

interface IFoo
{
    int Foo(int x);
}

class Program : IFoo
{
    const int Iterations = 1000000000;

    public int Foo(int x)
    {
        return x * 3;
    }

    static void Main(string[] args)
    {
        int x = 3;
        IFoo ifoo = new Program();
        Func<int, int> del = ifoo.Foo;
        // Make sure everything's JITted:
        ifoo.Foo(3);
        del(3);

        Stopwatch sw = Stopwatch.StartNew();        
        for (int i = 0; i < Iterations; i++)
        {
            x = ifoo.Foo(x);
        }
        sw.Stop();
        Console.WriteLine("Interface: {0}", sw.ElapsedMilliseconds);

        x = 3;
        sw = Stopwatch.StartNew();        
        for (int i = 0; i < Iterations; i++)
        {
            x = del(x);
        }
        sw.Stop();
        Console.WriteLine("Delegate: {0}", sw.ElapsedMilliseconds);
    }
}

результаты (.NET 3.5; .NET 4.0b2 примерно то же самое):

Interface: 5068
Delegate: 4404

теперь у меня нет особой веры, что это означает, что делегатам действительно быстрее, чем интерфейсы... но это убеждает меня. что они не на порядок медленнее. Кроме того, это почти ничего не делает в методе делегата/интерфейса. Очевидно, что стоимость вызова будет делать все меньше и меньше разницы, поскольку вы делаете все больше и больше работы за вызов.

следует быть осторожным в том, что вы не создаете новый делегат несколько раз, когда вы используете только один экземпляр интерфейса. Это мог бы вызовите проблему, так как это спровоцирует сбор мусора и т. д. Если вы используете метод экземпляра в качестве делегата в цикле вы найдете более эффективным объявить переменную делегата вне цикла, создать один экземпляр делегата и повторно использовать его. Например:

Func<int, int> del = myInstance.MyMethod;
for (int i = 0; i < 100000; i++)
{
    MethodTakingFunc(del);
}

более эффективна, чем:

for (int i = 0; i < 100000; i++)
{
    MethodTakingFunc(myInstance.MyMethod);
}

могла ли это быть проблема, которую вы видели?


поскольку CLR v 2, стоимость вызова делегата очень близка к стоимости вызова виртуального метода, который используется для методов интерфейса.

посмотреть Джоэл Pobar'ы.


Я считаю совершенно неправдоподобным, что делегат существенно быстрее или медленнее виртуального метода. Во всяком случае, делегат должен быть незначительно быстрее. На более низком уровне делегаты обычно реализуются примерно так (используя нотацию в стиле C, но, пожалуйста, простите любые незначительные синтаксические ошибки, поскольку это всего лишь иллюстрация):

struct Delegate {
    void* contextPointer;   // What class instance does this reference?
    void* functionPointer;  // What method does this reference?
}

вызов делегата работает примерно так:

struct Delegate myDelegate = somethingThatReturnsDelegate();
// Call the delegate in de-sugared C-style notation.
ReturnType returnValue = 
    (*((FunctionType) *myDelegate.functionPointer))(myDelegate.contextPointer);

класс, переведенный на C, будет чем-то например:

struct SomeClass {
    void** vtable;        // Array of pointers to functions.
    SomeType someMember;  // Member variables.
}

чтобы вызвать функцию vritual, вы должны сделать следующее:

struct SomeClass *myClass = someFunctionThatReturnsMyClassPointer();
// Call the virtual function residing in the second slot of the vtable.
void* funcPtr = (myClass -> vtbl)[1];
ReturnType returnValue = (*((FunctionType) funcPtr))(myClass);

они в основном одинаковы, за исключением того, что при использовании виртуальных функций вы проходите через дополнительный слой косвенности, чтобы получить указатель функции. Однако этот дополнительный косвенный слой часто свободен, потому что современные предсказатели ветвей процессора будут угадывать адрес указателя функции и спекулятивно выполнять его цель параллельно с поиском адреса функции. Я обнаружено (хотя и в D, а не в C#), что вызовы виртуальных функций в узком цикле не медленнее, чем прямые вызовы без инлайна, при условии, что для любого заданного запуска цикла они всегда разрешаются одной и той же реальной функцией.


Я сделал несколько тестов (в .Net 3.5... позже я проверю дома, используя .Net 4). Дело в том,: Получение объекта в качестве интерфейса и последующее выполнение метода быстрее, чем получение делегата из метода, а затем вызов делегата.

учитывая, что переменная уже находится в правильном типе (интерфейс или делегат), и простой вызов делает делегат победителем.

по какой-то причине получение делегата над методом интерфейса (возможно, над любым виртуальным методом) очень замедлившийся.

и, учитывая, что есть случаи, когда мы просто не можем предварительно сохранить делегат (например, в диспетчерах), это может оправдать, почему интерфейсы быстрее.

вот результаты:

чтобы получить реальные результаты, скомпилируйте это в режиме выпуска и запустите его за пределами Visual Studio.

проверка прямых вызовов дважды
00: 00: 00.5834988
00: 00: 00.5997071

проверка вызовов интерфейса, получение интерфейс при каждом вызове
00: 00: 05.8998212

проверка вызовов интерфейса, получение интерфейса один раз
00: 00: 05.3163224

проверка вызовов действий (делегатов), получение действия при каждом вызове
00: 00: 17.1807980

проверка вызовов действия (делегата), получение действия один раз
00: 00: 05.3163224

проверка действия (делегата) над методом интерфейса, получение обоих каждый звонок
00:03:50.7326056

проверка действия (делегата) над методом интерфейса, получение интерфейс один раз, делегат при каждом вызове
00:03:48.9141438

проверка действия (делегата) над методом интерфейса, получение обоих один раз
00: 00: 04.0036530

Как вы можете видеть, прямые вызовы очень быстро. Сохранение интерфейса или делегата раньше, а затем только вызов его очень быстро. Но необходимость получить делегата медленнее чем получать интерфейс. Необходимость получения делегата над методом интерфейса (или виртуальным методом, не уверен) очень медленная (сравните 5 секунд получения объекта в качестве интерфейса с почти 4 минутами того же, чтобы получить действие).

код, который сгенерировал эти результаты, находится здесь:

using System;

namespace ActionVersusInterface
{
    public interface IRunnable
    {
        void Run();
    }
    public sealed class Runnable:
        IRunnable
    {
        public void Run()
        {
        }
    }

    class Program
    {
        private const int COUNT = 1700000000;
        static void Main(string[] args)
        {
            var r = new Runnable();

            Console.WriteLine("To get real results, compile this in Release mode and");
            Console.WriteLine("run it outside Visual Studio.");

            Console.WriteLine();
            Console.WriteLine("Checking direct calls twice");
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    r.Run();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    r.Run();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking interface calls, getting the interface at every call");
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    IRunnable interf = r;
                    interf.Run();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking interface calls, getting the interface once");
            {
                DateTime begin = DateTime.Now;
                IRunnable interf = r;
                for (int i = 0; i < COUNT; i++)
                {
                    interf.Run();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) calls, getting the action at every call");
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    Action a = r.Run;
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) calls, getting the Action once");
            {
                DateTime begin = DateTime.Now;
                Action a = r.Run;
                for (int i = 0; i < COUNT; i++)
                {
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }


            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) over an interface method, getting both at every call");
            {
                DateTime begin = DateTime.Now;
                for (int i = 0; i < COUNT; i++)
                {
                    IRunnable interf = r;
                    Action a = interf.Run;
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) over an interface method, getting the interface once, the delegate at every call");
            {
                DateTime begin = DateTime.Now;
                IRunnable interf = r;
                for (int i = 0; i < COUNT; i++)
                {
                    Action a = interf.Run;
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }

            Console.WriteLine();
            Console.WriteLine("Checking Action (delegate) over an interface method, getting both once");
            {
                DateTime begin = DateTime.Now;
                IRunnable interf = r;
                Action a = interf.Run;
                for (int i = 0; i < COUNT; i++)
                {
                    a();
                }
                DateTime end = DateTime.Now;
                Console.WriteLine(end - begin);
            }
            Console.ReadLine();
        }
    }

}

насчет того, что делегаты являются контейнерами? Разве возможность многоадресной рассылки не добавляет накладных расходов? Пока мы находимся на этой теме, что, если мы продвинем этот аспект контейнера немного дальше? Ничто не запрещает нам, если D является делегатом, выполнять d += d; или строить произвольно сложный направленный граф (указатель контекста, указатель метода) пар. Где я могу найти документацию, описывающую, как этот график проходит при вызове делегата?