Поделиться через


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.

Comments

  • Anonymous
    April 01, 2004
    Some quick questions:

    You mention that using LCG to put arguments on the stack is "orders of magnitude faster"-- can you quantify this? And what about the overhead of the one-off LCG to generate the stub? What do you think the implications of LCG for e.g. JScript or IronPython are?

    In theory, can't I define a delegate B Arrow<A,B>(A arg) (and so on for methods with more arguments) and get better typing to boot? What is the relative efficiency of this? And will the BCL have built-in Arrow<A,B> and Tuple<...> types?

    Lastly, a couple of general LCG questions: Can I use LCG e.g. to emit wrappers that do security assertions or something like that? And how does LCG work in long-running processes, or where you use LCG a lot-- is LCG-generated code GCed?

  • Anonymous
    April 13, 2004
    I recently mentioned Joel Pobar's posting on Late-Bound Invocation and over the weekend I did some investigation into what the performance is like for the various methods he describes. The spur for this was reading about the JVM-based scripting language...

  • Anonymous
    April 13, 2004
    Eric Gunnerson has an article on MSDN comparing the perf for these different approaches, and what Whidbey should improve:
    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp02172004.asp

  • Anonymous
    April 13, 2004
    Firstly, thanks to Charles for the performance numbers. I hadn't had a chance to get to them myself. The delegate and DynamicMethod PDC bits will be slightly less performant than the next community drop, so expect the numbers to change a little. There's also mention of the cost of generating the DynamicMethod - yes, absolutely, there's a working set hit with the introduction of the JIT.

    Secondly, I also need to clean up the example a little more. I tried to blend both performance and type checking aspects in to the example, but it ended up just looking kludgy, as you can see.

    Thirdly, there's a bunch of things I didn't explain. I should really go away and cook up another blog post, but I'll dump here for now. Clearly, the scenario is what drives the choice in invocation path. In this particular case, having full knowledge of the receiver gave us a clear performance win. Unfortunately, as with most common dynamic scenario's, you never generally have full knowledge of anything. With this statement, comes a whole load of baggage. Signature checks, type checks, receiver checks - sounds a little like what Invoke member is for doesn't it?

    There's a whole range of scenario's I haven't touched on here, some include:
    - You know the signature of the method to invoke, but you don't know the name or the receiver.
    - You know the signature and the name, but not the receiver
    - You know the name, but not the signature
    Perhaps my next blog post will touch on these - seems like I've opened a can of worms so it might just be worthwhile.

    Lastly, Dominic had a question about LCG in long-running processes. The long and the short of it is, LCG is GC'ed, there's the reclaimation story. So definitely think about holding on to that handle if you care about throughput.

    That's enough dumping for now. Leave me a comment if you'd like a blog story for all this. =)

  • Anonymous
    June 17, 2004
    Visual Studio 2005 will contain some exciting enhancements to the System.Reflection namespace.

  • Anonymous
    August 05, 2004
    What about building a special purpose "dynamic calling assembly" with an only class/method, programmed in plain IL, that takes a RuntimeMethodHandle instance and an Object[] as parameters, does the stack pushing and finally invokes via "calli"? I'm planning to take this road in my dynamic invocation scenario (I know the method token when trying to do the calling), but, as far as I have read, there's no way to do an "indirect newobj" constructor call. Isn't there an equivalente to calli that allocates memory for a new object? not even in Whidbey?

    TIA,
    Alan

  • Anonymous
    March 31, 2005
    Lightweight Code Generation

  • Anonymous
    July 01, 2005
    There seems to be a fair amount of recent press and blog action surrounding the dynamic or “scripting”...

  • Anonymous
    July 01, 2005
    There seems to be a fair amount of recent press and blog action surrounding the dynamic or “scripting”...

  • Anonymous
    March 24, 2006
       
           Q: Assume we have 2000 unknown types; (however) we know each type has a constructor
    ...

  • Anonymous
    October 08, 2006
    Have you ever tried DynamicMethod, one of the coolest features in the .NET 2.0? Hope you have; otherwise,

  • Anonymous
    May 01, 2007
    I am not at this conference, but anyway...What I was wondering is how Silverlight does support these...

  • Anonymous
    May 26, 2009
    PingBack from http://castironbakeware.info/story.php?title=joel-pobar-s-clr-weblog-late-bound-invocation-notes-callvirt

  • Anonymous
    May 31, 2009
    PingBack from http://indoorgrillsrecipes.info/story.php?id=2362

  • Anonymous
    June 08, 2009
    PingBack from http://insomniacuresite.info/story.php?id=8012