Late-bound invocation notes - CallVirt, Delegates, DynamicMethod, InvokeMember.

I've been cooking up some notes on the ways one may do late-bound or dynamic invocation. It's unpolished, but hopefully you can dig yourself out of the weeds to get something out of it. Don't expect it to be complete, but if there's enough interest, I'll invest the time to polish it up.

Invocation

Invocation usually involves the binding to, and invoking of a method (the implementation of an operation for certain types in the runtime). There is an obvious spectrum for the ways one may invoke a method, and this is usually driven by the scenario of application. We have the ability to invoke from the traditional static compile time scenario, and at runtime via various Reflection API offerings. There are particular performance tradeoffs for each slice of the spectrum and it is worthwhile understanding each.

From a high level, the spectrum looks something like:

CallVirt -- Delegate -- DynamicMethod (LCG) -- InvokeMember

CallVirt

In terms of method invocation, one may either encode the destination as part of the instruction, or compute it at runtime. As background, the destination of a CallVirt call is computed via the method token and value of the first argument “this”. You’re then at the mercy of the runtime to find the best resolved case for destination (overload resolution etc).

The scenario for dynamic invocation in terms of CallVirt is simple. Define a base class, or an interface, and force your target to implement. Once you’ve obtained a reference to your late bound object, you’re then able to cast to your base class or interface and invoke. This is somewhat of a restriction on your applications late bound story because you’re enforcing a contract, but it does have a huge performance win.

Note: Specifying a contract via an Interface is more relaxed than asking the target to derive from a base type. As of Whidbey, the performance of each invocation is pretty similar.

Delegate

OO version of function pointers, typesafe and secure. The Whidbey delegate story has been overhauled in terms of performance, now becoming very close to a CallVirt invocation. Knowing the signature at compile time is well understood and well defined, however binding delegates at runtime more costly. A DynamicInvoke call may incur the overhead of binding checks to the type (when passing in a string based method name), checks for the Invoke() member, and the cost of new’ing up an object array with the relevant arguments. It’s also worthwhile to note that delegate calls to instance methods will be faster than static methods - static method delegate invocation incurs the cost of a shuffle thunk (shuffling the “this" pointer off the stack).

Generally, if you’re creating delegates late-bound, it’s best to avoid binding in the CreateDelegate() call, by providing a MethodInfo. You’re able to then drop the MethodInfo and hold on to your delegate. Invocation from that point on is fairly fast.

Note: Delegates are not able to be created over contructors.

DynamicMethod (Lightweight Code Gen)

This story for invocation is the more interesting one. Cooking up a Dynamic method that takes an object array (instance + parameters) and then wraps a method call is orders of magnitude faster than invocation over XXXInfo’s, latebound invokes over Delegate.DynamicInvoke(), and even DynamicMethod.Invoke() itself. What you’re effectively doing in the DynamicMethod is setting up the stack for a call via runtime code generation, using parameters from the object[] args array. This invocation scenario allows much greater throughput that the traditional CreateDelegate() and XXXInfo.Invoke(), yet still supplies the same delegate semantic.

The code for this particular case is fairly interesting:

using System;

using System.Reflection;

using System.Reflection.Emit;

using System.Runtime.CompilerServices;

class Foo

{

      [MethodImplAttribute(MethodImplOptions.NoInlining)]

      public string MyMethod(string x)

      {

            Console.WriteLine(x);

            return x;

      }

}

class DMInvokeWrapperExample

{

      delegate object MyGenericDelegate(object[] args);

      static void Main(string[] args)

      {

            Foo foo = new Foo();

            // DynamicMethod wrapper method

            DynamicMethod dm = new DynamicMethod("MyMethodWrapper", typeof(object), new Type[] { typeof(object[]) }, typeof(DMInvokeWrapperExample), true);

            ILGenerator il = dm.GetILGenerator();

            Label l1 = il.DefineLabel();

            LocalBuilder returnLocal = il.DeclareLocal(typeof(object));

            // grab the method parameters of the method we wish to wrap

            ParameterInfo[] methodParameters = typeof(Foo).GetMethod("MyMethod").GetParameters();

            int parameterLength = methodParameters.Length;

            MethodInfo method = typeof(Foo).GetMethod("MyMethod");

      // check to see if the call to MyMethodWrapper has the required amount of arguments in the object[] array.

            il.Emit(OpCodes.Ldarg_0);

            il.Emit(OpCodes.Ldlen);

            il.Emit(OpCodes.Conv_I4);

            il.Emit(OpCodes.Ldc_I4, parameterLength + 1);

            il.Emit(OpCodes.Beq_S, l1);

            il.Emit(OpCodes.Ldstr, "insufficient arguments");

            il.Emit(OpCodes.Newobj, typeof(System.ArgumentException).GetConstructor(new Type[] { typeof(string) }));

            il.Emit(OpCodes.Throw);

            il.MarkLabel(l1);

            // pull out the Foo instance from the first element in the object[] args array

            il.Emit(OpCodes.Ldarg_0);

            il.Emit(OpCodes.Ldc_I4_0);

            il.Emit(OpCodes.Ldelem_Ref);

            // cast the instance to Foo

            il.Emit(OpCodes.Castclass, typeof(Foo));

            // pull out the parameters to the instance method call and push them on to the IL stack

            for (int i = 0; i < parameterLength; i++)

            {

                  il.Emit(OpCodes.Ldarg_0);

                  il.Emit(OpCodes.Ldc_I4, i + 1);

                  il.Emit(OpCodes.Ldelem_Ref);

                  // we've special cased it, for this code example

                  if (methodParameters[i].ParameterType == typeof(string))

                  {

                        il.Emit(OpCodes.Castclass, typeof(string));

                  }

                  // test or switch on parameter types, you'll need to cast to the respective type

                  // ...

            }

// call the wrapped method

            il.Emit(OpCodes.Call, method);

            // return what the method invocation returned

            il.Emit(OpCodes.Stloc, returnLocal);

            il.Emit(OpCodes.Ldloc, returnLocal);

            il.Emit(OpCodes.Ret);

            // cook up a delegate for the MyMethodWrapper DynamicMethod

            MyGenericDelegate d3 = (MyGenericDelegate)dm.CreateDelegate(typeof(MyGenericDelegate));

// invoke it

            d3(new object[] { foo, "hello, world!" });

      }

}

As you can see, we've created a DynamicMethod that takes a object[] array as it's parameter, and returns object. It matches the signature of MyGenericDelegate, so we're then able to use this generic delegate to make calls to methods we wrap, late-bound. We can pass that delegate around however we like. You can special case this, or make it generic - in this case, I've tried to illustrate a blending of both. The MyMethodWrapper DynamicMethod requires an instance to invoke the method on, in the first array position, and the method parameters to follow.

In this case, we've wrapped the Foo.MyMethod(string x) method in my DynamicMethod. The second last line of source cooks up the delegate to the MyMethodWrapper DynamicMethod. The last line invokes the delegate, with an instance of Foo, and its string argument “hello, world”. This particular case ends up being about one order of magnitude faster than a CreateDelegate().DynamicInvoke(...).

InvokeMember (Binding + Invocation)

For this particular section, I talk a lot about the MemberInfoCache - which I doubt has ever really been discussed before. We did have a Reflection cache (MemberInfoCache) in Everett, but we've tuned it up for Whidbey. Once we have the baked story, ready to go, I'll be sure to blog about it. We're hoping to produce some guidelines about Reflection performance, and the MemberInfoCache has a lot to do it. Nevertheless:

InvokeMember switches on MemberType, so there is a pile of checks before it even decides where to go. InvokeMember is the general case, if you know what you’re doing, it’s best to go down the straight path, bind and invoke yourself. Overloading resolution in the binding process is complicated, so even at best case, when the MemberInfo cache is populated, InvokeMember will still need to do a bunch of comparisons to find the best fit.

InvokeMember looses the MemberInfo reference, so binding becomes expensive when you’re doing a lot of repeated invocation. The MemberInfoCache will purge the local MemberInfo for the InvokeMember call, so expect the cache to churn. It’s best to hold on to that MemberInfo reference yourself. The MemberInfoCache is per process. So think about running your code in parallel with other code. Some other appdomain (or even something in your appdomain) may be calling GetMembers() and filling and disposing your cache from underneath you.