Может ли делегировать .DynamicInvoke в этом общем коде?

Этот вопрос частично связан с делегатами, а частично - с дженериками.

Учитывая упрощенный код:

internal sealed class TypeDispatchProcessor
{
    private readonly Dictionary<Type, Delegate> _actionByType 
        = new Dictionary<Type, Delegate>();

    public void RegisterProcedure<T>(Action<T> action)
    {
        _actionByType[typeof(T)] = action;
    }

    public void ProcessItem(object item)
    {
        Delegate action;
        if (_actionByType.TryGetValue(item.GetType(), out action))
        {
            // Can this call to DynamicInvoke be avoided?
            action.DynamicInvoke(item);
        }
    }
}

Я читаю в другом месте на SO, что вызов делегата напрямую (с круглыми скобками) на порядок быстрее, чем вызов DynamicInvoke, что имеет смысл.

В приведенном выше примере кода мне интересно, могу ли я выполнять проверку типов и как-то повысить производительность.

В некотором контексте: у меня есть поток объектов, которые обрабатываются различными обработчиками, и эти обработчики могут быть зарегистрированы/незарегистрированы во время выполнения. Вышеупомянутый шаблон отлично работает для моих целей, но я хотел бы сделать его более утонченным, если это возможно.

Один из вариантов - сохранить Action<object> в Dictionary и обернуть делегаты Action<T> другим делегатом. Я еще не сравнил изменение производительности, которое повлияет на этот второй косвенный вызов.

Ответ 1

Я сильно подозреваю, что перенос вызовов будет намного более эффективным, чем использование DynamicInvoke. Тогда ваш код будет выглядеть следующим образом:

internal sealed class TypeDispatchProcessor
{
    private readonly Dictionary<Type, Action<object>> _actionByType 
        = new Dictionary<Type, Action<object>>();

    public void RegisterProcedure<T>(Action<T> action)
    {
        _actionByType[typeof(T)] = item => action((T) item);
    }

    public void ProcessItem(object item)
    {
        Action<object> action;
        if (_actionByType.TryGetValue(item.GetType(), out action))
        {
            action(item);
        }
    }
}

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

Ответ 2

Итак, я сделал некоторые измерения на этом.

var delegates = new List<Delegate>();
var actions = new List<Action<object>>();

const int dataCount = 100;
const int loopCount = 10000;

for (int i = 0; i < dataCount; i++)
{
    Action<int> a = d => { };
    delegates.Add(a);
    actions.Add(o => a((int)o));
}

var sw = Stopwatch.StartNew();
for (int i = 0; i < loopCount; i++)
{
    foreach (var action in actions)
        action(i);
}
Console.Out.WriteLine("{0:#,##0} Action<object> calls in {1:#,##0.###} ms",
    loopCount * dataCount, sw.Elapsed.TotalMilliseconds);

sw = Stopwatch.StartNew();
for (int i = 0; i < loopCount; i++)
{
    foreach (var del in delegates)
        del.DynamicInvoke(i);
}
Console.Out.WriteLine("{0:#,##0} DynamicInvoke calls in {1:#,##0.###} ms",
    loopCount * dataCount, sw.Elapsed.TotalMilliseconds);

Я создал несколько элементов для косвенного вызова, чтобы избежать любой оптимизации, которую может выполнять JIT.

Результаты довольно убедительные!

1,000,000 Action calls in 47.172 ms
1,000,000 Delegate.DynamicInvoke calls in 12,035.943 ms

1,000,000 Action calls in 44.686 ms
1,000,000 Delegate.DynamicInvoke calls in 12,318.846 ms

Итак, в этом случае, подставляя вызов DynamicInvoke для дополнительного косвенного вызова, а приведение было примерно в 270 раз быстрее. Все в течение дня работают.

Ответ 3

Если вам нужно расширить это на вызовы элементов-членов из классов без использования Reflection.Emit, вы можете сделать это, создав серию подсказок компилятора, которые могут отображать класс и список параметров функции или тип возвращаемого значения.

В основном вам нужно создать lambdas, которые принимают объекты в качестве параметров и возвращают объект. Затем используйте общую функцию, которую компилятор видит AOT для создания кеша подходящих методов для вызова члена и передачи параметров. Хитрость состоит в том, чтобы создать открытых делегатов и передать их через вторую ламду, чтобы добраться до основного намека во время выполнения.

Вам нужно предоставить подсказки для каждого класса и подписи (но не для каждого метода или свойства).

Я разработал класс здесь, который делает это, это слишком долго, чтобы перечислять в этом сообщении.

При тестировании производительности это не так близко, как пример выше, но он является общим, что означает, что он работает в тех обстоятельствах, в которых я нуждался. Производительность около 4,5x при чтении свойства по сравнению с Invoke.