The power of generics compels you, the power of generics compels you...
Once again I've devoted some time to the exorcism of the daemons of Reflection
. This time, I'm chasing the cost of calling MethodInfo.Invoke
against an arbitrary (chosen at run-time) method.
In previous efforts, I've tried to give you generic sorting with as little cost as possible, added support for Enum
properties, improved upon it and added support for access to struct
s (i.e. ValueType
s) and Nullable<>
. Along the way, I learned a lot about IL and the rules for using Reflection.Emit
and DynamicMethod
for LCG.
Now the cost of calling any arbitrary method of a class isn't quite as apparent as when you're trying to do dynamic comparisons, but it's still significant. Using my new DynamicFunction
and DynamicProcedure
classes is really noticeable. Running against the same test-jig Person class and Animal struct used in the DynamicComparer
sample program, I get the following performance chart executing three methods against 500000 objects:
Method Elapsed time (seconds) Explanation Compile-time :00.305 The test method simply executes the method calls directly with no dynamic choices. This is the baseline for comparison, as good as it can get. Dynamic Strong :00.591 This is called using the strong-typed delegate form, where all the argument types are known and correctly specified. The specific methods called are specified by a string
method name.Dynamic Weak :00.776 This is called using the weak-typed delegate form, where all the arguments are passed as a params object[]
object, but are of the correct type. The specific methods called are specified by astring
method name.Reflection :18.243 This is called using the a standard MethodInfo.Invoke
, where all the arguments are passed as anew object[]
object, but are of the correct type. The specific methods called are specified by astring
method name. This is 31 times slower than the strong-type form!
While building this, I've extensively refactored the logic for the Reflection.Emit
into another class. The new DynamicEmit
encapsulates the ILGenerator
used during the synthesis of the DynamicMethod
in both the old DynamicComparer
and the new classes.
DynamicFunction
This class allows you to call any method (instance or static) of a class with any return type. There are two supported forms of the delegate.
- The first form is a weak-typed delegate that takes an
params object[]
for any arguments needed. This version is much faster than plan oldMethodInfo.Invoke
, but about 30% slower than the strong-typed delegate form. You instantiate and call the method like this, given a target method ofbool YourClass.MethodToCall(int, string)
:
The two arguments are automatically wrapped up by C# in a newYourClass target = new YourClass(); Func<T, bool> method = DynamicFunction<YourClass, bool>.Initialize('MethodToCall"); bool result = method(target, 1, "Hi");
object[]
and then unwrapped and coerced into the target method's types (if needed). The wrapping and unwrapping is the source of most of the performance difference between this and the strong-type form. - The second form is a strong-typed delegate that takes an
params object[]
for any arguments needed. This version is much faster than plan oldMethodInfo.Invoke
, but about 50% slower than directly coded calls, but gives you the obvious advantage of variability. You instantiate and call the method like this, given the same target method ofbool YourClass.MethodToCall(int, string)
:
The two arguments are now passed directly to the delegate, which will be coerced into the target method's types (if needed, not usually).YourClass target = new YourClass(); Func<T, bool, int, string> method = DynamicFunction<YourClass, bool, int, string>.Initialize('MethodToCall"); bool result = method(target, 1, "Hi");
DynamicProcedure
This class allows you to call any method (instance or static) of a class that doesn't return a value (e.g. it's a void
method). There are two supported forms of the delegate.
- The first form is a weak-typed delegate that takes an
params object[]
for any arguments needed. This version is much faster than plan oldMethodInfo.Invoke
, but about 30% slower than the strong-typed delegate form. You instantiate and call the method like this, given a target method ofvoid YourClass.VoidMethodToCall(int, string)
:
The two arguments are automatically wrapped up by C# in a newYourClass target = new YourClass(); Proc<T> method = DynamicProcedure<YourClass>.Initialize('VoidMethodToCall"); method(target, 1, "Hi");
object[]
and then unwrapped and coerced into the target method's types (if needed). The wrapping and unwrapping is the source of most of the performance difference between this and the strong-type form. - The second form is a strong-typed delegate that takes an
params object[]
for any arguments needed. This version is much faster than plan oldMethodInfo.Invoke
, but about 50% slower than directly coded calls, but gives you the obvious advantage of variability. You instantiate and call the method like this, given the same target method ofvoid YourClass.VoidMethodToCall(int, string)
:
The two arguments are now passed directly to the delegate, which will be coerced into the target method's types (if needed, not usually).YourClass target = new YourClass(); Proc<T, bool, int, string> method = DynamicProcedure<YourClass, int, string>.Initialize('MethodToCall"); bool result = method(target, 1, "Hi");
What does the code look like?
class Person { public Person(string name) { ... } public bool Compatible(Person potentialMate) { ... } public Person Breed(Person mate, Gender childGender) { ... } public void Mutate() { ... } } Func<Person, bool, Person> compatible = DynamicFunction<Person, bool, Person>.Initialize("Compatible"); Func<Person, Person, Person, Gender> breed = DynamicFunction<Person, Person, Person, Gender>.Initialize("Breed"); Proc<Person> mutate = DynamicProcedure<Person>.Initialize("Mutate"); Person child; if (compatible(new Person("Marc"), new Person("Beth")) child = breed(you, me, Gender.Female); else mutate(me);
What does the emitted IL code look like?
A sample of the emitted code, in weak-typed form call for the above bool Compatible(Person) looks like this:
IL_0000: /* 03 | */ ldarg.1 IL_0001: /* 16 | */ ldc.i4.0 IL_0002: /* 9a | */ ldelem.ref IL_0003: /* 74 | 02000002 */ castclass DynamicComparerSample.Person IL_0008: /* 0a | */ stloc.0 IL_0009: /* 02 | */ ldarg.0 IL_000a: /* 06 | */ ldloc.0 IL_000b: /* 28 | 0A000003 */ call Boolean Compatible(DynamicComparerSample.Person)/DynamicComparerSample.Person IL_0010: /* 2a | */ ret
A sample of the (much simpler) emitted code, in strong-typed form call for the above bool Compatible(Person) looks like this:
IL_0000: /* 02 | */ ldarg.0 IL_0001: /* 03 | */ ldarg.1 IL_0002: /* 28 | 0A000002 */ call Boolean Compatible(DynamicComparerSample.Person)/DynamicComparerSample.Person IL_0007: /* 2a | */ ret
As always, download here
7 comments:
Pedantry compels me to tell you that you spelled "compels" wrong. FYI.
I'm a moron and KNOW I can't spell. Spell checking the body is automatic, but the subject line? Thanks.
wow. I'm speechless. here I am trying to write myself some IL by hand to do what you've virtually automated via generics. I can see that I have a WHOLE lot to learn, but in the meantime, I think I'll have a look at Dynamic.cs and drink another diet mt. dew. Mahn! Very cool stuff, you win man.
Do you still have the test case for this project... I am running my own test and I couldn't replicate the result. I am testing the ToString() method for the Person class.
The test project is part of the Dynamic library. Head over to CodePlex and download the Dynamic library. By the way, ToString() is not necessarily a very quick method dues to all the string manipulation, so its execution time might be swamping the runtime of all frameworks. This is a hazard of any micro-benchmarks...
What about generic procedures and functions? I.e. T foo>T<( v1, v2, v3 ) or void bar>T<( T, T, T )?
Doing binding to generics isn't really much different, except you have to "fill in the blanks". I talk a little about that in this post about late-bound dynamic, but if you have some ideas of how I can make it more transparent (like with a fluent interface) please give me an idea.
Post a Comment