alternative for using slow DynamicInvoke on muticast delegate
Asked Answered
L

2

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)

Lister answered 16/8, 2011 at 19:6 Comment(3)
Can you cast your input delegates to a known delegate type to invoke them?Lewandowski
In some cases to EventHandler, but sadly not in all.Lister
See this question. Bottom line: you could use dynamic which is also very fast.Dede
A
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 ();
        }
    }
}
Astra answered 16/8, 2011 at 19:45 Comment(11)
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. :)Lister
Just FYI Linq Expression trees are available in .NET35. The above technique is doable in .NET2 as well but arguable more difficult.Astra
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.Astra
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.Lister
Just one more question: Is there a reason for introducing a delegate for calling CreateDelegate2 and not calling it directly?Lister
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).Astra
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.Astra
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. :(Lister
The problem should be reproducible. Just change EventArgs in the MyEventListener and MyEventSource classes to some other type which derives from EventArgs.Lister
Done. Also improved error-reporting of CreateDelegate function.Astra
How does performance compare in .NET 6 or .NET 8?Tillotson
L
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);
    }
}
Lewandowski answered 16/8, 2011 at 21:18 Comment(1)
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.Lister

© 2022 - 2024 — McMap. All rights reserved.