6

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)

nawfal
  • 70,104
  • 56
  • 326
  • 368
Emiswelt
  • 3,909
  • 1
  • 38
  • 56
  • 1
    Can you cast your input delegates to a known delegate type to invoke them? – Gabe Aug 16 '11 at 20:05
  • In some cases to EventHandler, but sadly not in all. – Emiswelt Aug 16 '11 at 20:22
  • See this [question](http://stackoverflow.com/questions/4751799/using-multicastdelegate-as-parameter-while-avoiding-dynamicinvoke). Bottom line: you could use `dynamic` which is also very fast. – nawfal Jun 04 '13 at 13:14

2 Answers2

11

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 ();
        }
    }
}
  • Thank you for your help. I'm on .Net 2.0, but I'll try to get into your concept and maybe switch to 4.0. Just give me some time to try it out. :) – Emiswelt Aug 16 '11 at 19:53
  • Just FYI Linq Expression trees are available in .NET35. The above technique is doable in .NET2 as well but arguable more difficult. – Just another metaprogrammer Aug 16 '11 at 20:01
  • I am a bit curious how things turned out for you. Did the .NET2 sample help you in anyway? I have some ideas on how to improve performance further in case you need it. – Just another metaprogrammer Aug 18 '11 at 11:39
  • Thank you for your adding a .NET2 sample. I really appreciate the hard work you have done. Just tested it and yes, the performance impact is great (x17). I'm currently in testing the thread safety, but from reading the code I think this should be no problem. Thank you. – Emiswelt Aug 20 '11 at 07:48
  • Just one more question: Is there a reason for introducing a delegate for calling CreateDelegate2 and not calling it directly? – Emiswelt Aug 20 '11 at 08:11
  • If I understand you correctly, the reason I use a delegate in static field is to avoid the creation of that delegate everytime GetOrAdd is called (it gives a small but measureable impact in this case). What you can do is inline the whole GetOrAdd function, that is just an artifact from when I used ConcurrentDictionary (.NET4). – Just another metaprogrammer Aug 20 '11 at 08:52
  • Btw. thinking about it, if you control of the event sources and they are quite few what you can do is to move the test of ISynchronizeInvoke into add/remove action of the event source. – Just another metaprogrammer Aug 20 '11 at 08:57
  • Thanks for the info. :) During testing in a simulated production environment, another problem appeared: There are events which do not have EventArgs as second param, but objects which are derived from EventArgs. When I try to call the CachedDelegate in this case a VerificationException occurs (Operation could destabilize runtime). When I try to dynamically load the parameter types for creating the dynamic method to exactly match the parameters of the method to call, a ArgumentException occurs (Error binding target to method). The event sources are many, and I sadly do not control all. :( – Emiswelt Aug 20 '11 at 09:26
  • The problem should be reproducible. Just change EventArgs in the MyEventListener and MyEventSource classes to some other type which derives from EventArgs. – Emiswelt Aug 20 '11 at 09:29
  • 2
    Done. Also improved error-reporting of CreateDelegate function. – Just another metaprogrammer Aug 20 '11 at 10:54
  • How does performance compare in .NET 6 or .NET 8? – jjxtra Jul 03 '23 at 15:52
3

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);
    }
}
Gabe
  • 84,912
  • 12
  • 139
  • 238
  • This is a nice idea. I used a little modification of this together with the solution of FuleSnabel to boost the internal workings of my program a little further by checking for a specific target type and calling a method on the invocation target directly. – Emiswelt Aug 20 '11 at 07:51