可以在此通用代码中避免Delegate.DynamicInvoke吗?

时间:2009-07-12 13:50:21

标签: c# .net generics delegates dynamic-invoke

这个问题部分是关于代表,部分是关于泛型。

鉴于简化代码:

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

我读elsewhere on SO直接调用委托(带括号)比调用DynamicInvoke快几个数量级,这是有道理的。

对于上面的代码示例,我想知道我是否可以执行类型检查并以某种方式提高性能。

某些上下文:我有一个对象流,这些对象可以在各种处理程序中运行,这些处理程序可以在运行时注册/取消注册。上述模式完全符合我的目的,但如果可能的话,我想让它变得更加快捷。

一个选项是将Action<object>存储在Dictionary中,并将Action<T>个委托与另一个委托包装在一起。我还没有比较第二次间接调用会影响的性能变化。

3 个答案:

答案 0 :(得分:23)

我强烈怀疑包装调用比使用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必须使用反射等检查所有参数,而不是包装代理中的简单强制转换。

答案 1 :(得分:7)

所以我做了一些测量。

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倍。所有这一切都在一天的工作中。

答案 2 :(得分:1)

如果需要在不使用Reflection.Emit的情况下将其扩展到包装成员调用,可以通过创建一系列可以映射类和函数参数列表或返回类型的编译器提示来实现。

基本上你需要创建lambdas,它将对象作为参数并返回一个对象。然后使用编译器看到AOT的泛型函数来创建适当方法的缓存来调用成员并转换参数。诀窍是创建开放的委托并通过第二个lamda传递它们以在运行时获取基础提示。

您必须为每个类和签名提供提示(但不是每个方法或属性)。

我已经编写了一个类here来完成这项工作,在这篇文章中列出的内容有点太长了。

在性能测试中,它并不像上面的例子那么好,但它是通用的,这意味着它可以在我需要的情况下工作。与Invoke相比,读取属性的性能约为4.5倍。