Выполнение вызовов делегатов против методов

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

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

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


РЕДАКТИРОВАТЬ. Чтобы уточнить, что я имею в виду делегатами и обратными интерфейсами.

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

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

Ответ 1

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

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

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 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);
}

Возможно, это была проблема, которую вы видели?

Ответ 2

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

Смотрите блог Joel Pobar.

Ответ 3

Я считаю совершенно неправдоподобным, что делегат существенно быстрее или медленнее, чем виртуальный метод. В любом случае делегат должен быть пренебрежимо быстрее. На более низком уровне делегаты обычно реализуют что-то вроде (используя нотацию 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, а не С#), что вызовы виртуальных функций в узком цикле не являются медленными, чем неинтенсивные прямые вызовы, при условии, что для любого заданного цикла цикла они всегда решаются на одну и ту же реальную функцию.

Ответ 4

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

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

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

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

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

Чтобы получить реальные результаты, скомпилируйте это в режиме выпуска и запустите его за пределами 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();
        }
    }

}

Ответ 5

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

Ответ 6

Скамьи-тесты можно найти здесь.