Производительность вызова делегатов против методов - PullRequest
59 голосов
/ 18 января 2010

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

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

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


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

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

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

Ответы [ 5 ]

74 голосов
/ 18 января 2010

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

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

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

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

20 голосов
/ 18 января 2010

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

См. Блог Джоэла Побара .

18 голосов
/ 18 января 2010

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

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

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

6 голосов
/ 19 марта 2012

Я провел несколько тестов (в .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

Проверка вызовов Action (делегатов), получение Action один раз
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();
        }
    }

}
1 голос
/ 29 марта 2011

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

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