alternative for using slow DynamicInvoke on muticast delegate

7.3k views Asked by At

I have the following piece of code in a base-class:

public static void InvokeExternal(Delegate d, object param, object sender)
{
    if (d != null)
    {
        //Check each invocation target
        foreach (Delegate dDelgate in d.GetInvocationList())
        {
            if (dDelgate.Target != null && dDelgate.Target is System.ComponentModel.ISynchronizeInvoke
                && ((System.ComponentModel.ISynchronizeInvoke)(dDelgate.Target)).InvokeRequired)
            {
                //If target is ISynchronizeInvoke and Invoke is required, invoke via ISynchronizeInvoke
                ((System.ComponentModel.ISynchronizeInvoke)(dDelgate.Target)).Invoke(dDelgate, new object[] { sender, param });
            }
            else
            {
                //Else invoke dynamically
                dDelgate.DynamicInvoke(sender, param);
            }
        }
    }
}

This code sample is responsible for invoking an event, represented as multicast delegate, where the invocation targets include small classes which do not care about cross-threading, but also classes which implement ISynchronizeInvoke and care a lot about cross-threading, like Windows Forms Controls.

In theory, this snippet works pretty fine, no errors occur. But the DynamicInvoke is incredibly slow, not to say it's the current bottleneck of the application.

So, there goes my question: Is there any way to speed up this little function without breaking the functionally to subscribe to the event directly?

The signature of all events/delegates is (object sender, EventArgs param)

2

There are 2 answers

11
Just another metaprogrammer On BEST ANSWER

If dDelegate is a known type (ie Action) you could always cast to it and call it directly.

With that said if you are on .NET3.5 you can use Expression trees to get a fair bit of optimization. My example uses the concurrent dictionary in .NET4 but that's replacable with a normal dictionary and a lock.

The idea is as following: The delegate holds which method it's calling to. For each unique method that is called I create (using Expression trees) a compiled delegate that calls that specific method. Creating a compiled delegate is expensive that's why it's important to cache it but once created the compiled delegate is as fast as a normal delegate.

On my machine 3,000,000 calls took 1 sec with the compiled delegate and 16 sec with DynamicInvoke.

// Comment this line to use DynamicInvoke instead as a comparison
#define USE_FAST_INVOKE


namespace DynInvoke
{
    using System;
    using System.Collections.Concurrent;
    using System.Linq.Expressions;
    using System.Reflection;

    static class Program
    {
        delegate void CachedMethodDelegate (object instance, object sender, EventArgs param);

        readonly static ConcurrentDictionary<MethodInfo, CachedMethodDelegate> s_cachedMethods =
            new ConcurrentDictionary<MethodInfo, CachedMethodDelegate> ();

        public static void InvokeExternal(Delegate d, object sender, EventArgs param)
        {
            if (d != null)
            {
                //Check each invocation target            
                foreach (var dDelgate in d.GetInvocationList())
                {
                    if (
                            dDelgate.Target != null
                        &&  dDelgate.Target is System.ComponentModel.ISynchronizeInvoke
                        &&  ((System.ComponentModel.ISynchronizeInvoke)(dDelgate.Target)).InvokeRequired
                        )
                    {
                        //If target is ISynchronizeInvoke and Invoke is required, invoke via ISynchronizeInvoke                    
                        ((System.ComponentModel.ISynchronizeInvoke)(dDelgate.Target)).Invoke(dDelgate, new object[] { sender, param });
                    }
                    else
                    {
#if USE_FAST_INVOKE
                        var methodInfo = dDelgate.Method;

                        var del = s_cachedMethods.GetOrAdd (methodInfo, CreateDelegate);

                        del (dDelgate.Target, sender, param);
#else
                        dDelgate.DynamicInvoke (sender, param);
#endif
                    }
                }
            }
        }

        static CachedMethodDelegate CreateDelegate (MethodInfo methodInfo)
        {
            var instance = Expression.Parameter (typeof (object), "instance");
            var sender = Expression.Parameter (typeof (object), "sender");
            var parameter = Expression.Parameter (typeof (EventArgs), "parameter");

            var lambda = Expression.Lambda<CachedMethodDelegate>(
                Expression.Call (
                    Expression.Convert (instance, methodInfo.DeclaringType),
                    methodInfo,
                    sender,
                    parameter
                    ),
                instance,
                sender,
                parameter
                );

            return lambda.Compile ();
        }

        class MyEventListener
        {
            public int Count;

            public void Receive (object sender, EventArgs param)
            {
                ++Count;
            }
        }

        class MyEventSource
        {
            public event Action<object, EventArgs> AnEvent;

            public void InvokeAnEvent (EventArgs arg2)
            {
                InvokeExternal (AnEvent, this, arg2);
            }
        }

        static void Main(string[] args)
        {

            var eventListener = new MyEventListener ();
            var eventSource = new MyEventSource ();

            eventSource.AnEvent += eventListener.Receive;

            var eventArgs = new EventArgs ();
            eventSource.InvokeAnEvent (eventArgs);

            const int Count = 3000000;

            var then = DateTime.Now;

            for (var iter = 0; iter < Count; ++iter)
            {
                eventSource.InvokeAnEvent (eventArgs);
            }

            var diff = DateTime.Now - then;

            Console.WriteLine (
                "{0} calls took {1:0.00} seconds (listener received {2} calls)", 
                Count, 
                diff.TotalSeconds,
                eventListener.Count
                );

            Console.ReadKey ();
        }
    }
}

Edit: As OP uses .NET2 I added an example that should be compatible with .NET2 runtime (as I use VS2010 I might use some new language features by mistake but I did compile using .NET2 runtime).

// Comment this line to use DynamicInvoke instead as a comparison
#define USE_FASTER_INVOKE

namespace DynInvoke
{
    using System;
    using System.Globalization;
    using System.Reflection.Emit;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Reflection;

    static class FasterInvoke
    {
        delegate void CachedMethodDelegate (object instance, object sender, EventArgs param);

        readonly static Dictionary<MethodInfo, CachedMethodDelegate> s_cachedMethods =
            new Dictionary<MethodInfo, CachedMethodDelegate> ();

        public static void InvokeExternal (Delegate d, object sender, EventArgs param)
        {
            if (d != null)
            {
                Delegate[] invocationList = d.GetInvocationList ();
                foreach (Delegate subDelegate in invocationList)
                {
                    object target = subDelegate.Target;
                    if (
                        target != null
                        && target is ISynchronizeInvoke
                        && ((ISynchronizeInvoke)target).InvokeRequired
                        )
                    {
                        ((ISynchronizeInvoke)target).Invoke (subDelegate, new[] { sender, param });
                    }
                    else
                    {
#if USE_FASTER_INVOKE
                        MethodInfo methodInfo = subDelegate.Method;

                        CachedMethodDelegate cachedMethodDelegate;
                        bool result;

                        lock (s_cachedMethods)
                        {
                            result = s_cachedMethods.TryGetValue (methodInfo, out cachedMethodDelegate);
                        }

                        if (!result)
                        {
                            cachedMethodDelegate = CreateDelegate (methodInfo);
                            lock (s_cachedMethods)
                            {
                                s_cachedMethods[methodInfo] = cachedMethodDelegate;
                            }
                        }

                        cachedMethodDelegate (target, sender, param);
#else
                        subDelegate.DynamicInvoke (sender, param);
#endif
                    }
                }
            }
        }

        static CachedMethodDelegate CreateDelegate (MethodInfo methodInfo)
        {
            if (!methodInfo.DeclaringType.IsClass)
            {
                throw CreateArgumentExceptionForMethodInfo (
                    methodInfo, 
                    "Declaring type must be class for method: {0}.{1}"
                    );
            }


            if (methodInfo.ReturnType != typeof (void))
            {
                throw CreateArgumentExceptionForMethodInfo (
                    methodInfo,
                    "Method must return void: {0}.{1}"
                    );
            }

            ParameterInfo[] parameters = methodInfo.GetParameters ();
            if (parameters.Length != 2)
            {
                throw CreateArgumentExceptionForMethodInfo (
                    methodInfo,
                    "Method must have exactly two parameters: {0}.{1}"
                    );
            }


            if (parameters[0].ParameterType != typeof (object))
            {
                throw CreateArgumentExceptionForMethodInfo (
                    methodInfo,
                    "Method first parameter must be of type object: {0}.{1}"
                    );
            }

            Type secondParameterType = parameters[1].ParameterType;
            if (!typeof (EventArgs).IsAssignableFrom (secondParameterType))
            {
                throw CreateArgumentExceptionForMethodInfo (
                    methodInfo,
                    "Method second parameter must assignable to a variable of type EventArgs: {0}.{1}"
                    );
            }

            // Below is equivalent to a method like this (if this was expressible in C#):
            //  void Invoke (object instance, object sender, EventArgs args)
            //  {
            //      ((<%=methodInfo.DeclaringType%>)instance).<%=methodInfo.Name%> (
            //          sender,
            //          (<%=secondParameterType%>)args
            //          );
            //  }

            DynamicMethod dynamicMethod = new DynamicMethod (
                String.Format (
                    CultureInfo.InvariantCulture,
                    "Run_{0}_{1}",
                    methodInfo.DeclaringType.Name,
                    methodInfo.Name
                    ),
                null,
                new[]
                    {
                        typeof (object),
                        typeof (object),
                        typeof (EventArgs)
                    },
                true
                );

            ILGenerator ilGenerator = dynamicMethod.GetILGenerator ();
            ilGenerator.Emit (OpCodes.Ldarg_0);
            ilGenerator.Emit (OpCodes.Castclass, methodInfo.DeclaringType);
            ilGenerator.Emit (OpCodes.Ldarg_1);
            ilGenerator.Emit (OpCodes.Ldarg_2);
            ilGenerator.Emit (OpCodes.Isinst, secondParameterType);
            if (methodInfo.IsVirtual)
            {
                ilGenerator.EmitCall (OpCodes.Callvirt, methodInfo, null);                
            }
            else
            {
                ilGenerator.EmitCall (OpCodes.Call, methodInfo, null);                
            }
            ilGenerator.Emit (OpCodes.Ret);

            return (CachedMethodDelegate)dynamicMethod.CreateDelegate (typeof (CachedMethodDelegate));
        }

        static Exception CreateArgumentExceptionForMethodInfo (
            MethodInfo methodInfo, 
            string message
            )
        {
            return new ArgumentException (
                String.Format (
                    CultureInfo.InvariantCulture,
                    message,
                    methodInfo.DeclaringType.FullName,
                    methodInfo.Name
                    ),
                "methodInfo"
                );
        }
    }

    static class Program
    {
        class MyEventArgs : EventArgs
        {

        }

        class MyEventListener
        {
            public int Count;

            public void Receive (object sender, MyEventArgs param)
            {
                ++Count;
            }
        }

        delegate void MyEventHandler (object sender, MyEventArgs args);

        class MyEventSource
        {
            public event MyEventHandler AnEvent;

            public void InvokeAnEvent (MyEventArgs arg2)
            {
                FasterInvoke.InvokeExternal (AnEvent, this, arg2);
            }
        }

        static void Main (string[] args)
        {
            MyEventListener eventListener = new MyEventListener ();
            MyEventSource eventSource = new MyEventSource ();

            eventSource.AnEvent += eventListener.Receive;

            MyEventArgs eventArgs = new MyEventArgs ();
            eventSource.InvokeAnEvent (eventArgs);

            const int count = 5000000;

            DateTime then = DateTime.Now;

            for (int iter = 0; iter < count; ++iter)
            {
                eventSource.InvokeAnEvent (eventArgs);
            }

            TimeSpan diff = DateTime.Now - then;

            Console.WriteLine (
                "{0} calls took {1:0.00} seconds (listener received {2} calls)",
                count,
                diff.TotalSeconds,
                eventListener.Count
                );

            Console.ReadKey ();
        }
    }
}
1
Gabe On

If you have a set of known types, you can check for them first, and only revert to DynamicInvoke if you didn't know the type at compile time.

// delegate is most likely to be EventHandler
var e1 = dDelegate as EventHandler;
if (e1 != null)
    e1(sender, param);
else
{
    // might be DelegateType2
    var d2 = dDelegate as DelegateType2;
    if (d2 != null)
        d2(sender, param);
    else
    {
        // try DelegateType3
        var d3 = dDelegate as DelegateType3;
        if (d3 != null)
            d3(sender, param);
        else
            // last resort
            dDelgate.DynamicInvoke(sender, param);
    }
}