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

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

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

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


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

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

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

59
задан Community 23 May 2017 в 11:33
поделиться

3 ответа

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

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

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

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

75
ответ дан 24 November 2019 в 18:18
поделиться

Я разработал этот метод здесь, работает до ТБ.

private static string FormatBytes(long bytes)
{
    string[] Suffix = { "B", "KB", "MB", "GB", "TB" };
    int i;
    double dblSByte = bytes;
    for (i = 0; i < Suffix.Length && bytes >= 1024; i++, bytes /= 1024) 
    {
        dblSByte = bytes / 1024.0;
    }

    return String.Format("{0:0.##} {1}", dblSByte, Suffix[i]);
}
-121--2091414-

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

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

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

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

-121--960468-

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

Смотрите блог Джоэла Побара .

21
ответ дан 24 November 2019 в 18:18
поделиться

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

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

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

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

19
ответ дан 24 November 2019 в 18:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: