Click here to Skip to main content
15,884,237 members
Articles / Programming Languages / C#

generic Method-access via Linq.Expressions instead of Reflection

Rate me:
Please Sign up or sign in to vote.
4.45/5 (13 votes)
28 Oct 2017CPOL5 min read 18.1K   116   8   9
10-times faster Accesss with Reflection.MethodInfo (updated)

Sorry - the attached Code has turned out as outdated

Please first refer to Dismembers Article-Comment He gives the advice, that what I have developed by myself, to "compile" MethodInfos to anonymous methods, using Linq.Expression - that already exists as Member of the MethodInfo-Class - namly as MethodInfo.CreateDelegate<TDelegate>().

Now I leave the article as it was - maybe you have fun to see a bit of operating on Linq.Expressions, and glimpse a bit to some of its concepts.

But don't take the attached sources as practical problem-solving of anything. Instead of my
MethodInfo.MakeCompiledMethod<TDelegate>() - Extension-Method
imperativly simply use the built-in
MethodInfo.CreateDelegate<TDelegate>() - Instance-Method.

Maybe now I should delete this article, but as the readers interests shows, only very few programmers know MethodInfo.CreateDelegate<TDelegate>()

So although the article in a way has lost it's code to share, it still may help increase the level of awareness of MethodInfo.CreateDelegate() - which can be seen as a helpful service to the communities Knowledge too.

But now the article as it was, before I learned to know .CreateDelegate():

Code First ;-)

C#
public static Delegate MakeCompiledMethod(this MethodInfo mtd) {
   if (mtd == null) throw new ArgumentNullException("ReflectionX.MakeCompiledMethod(MethodInfo mtd): mtd mustn't be null");
   var prams = mtd.GetParameters().Select(p => Expression.Parameter(p.ParameterType, p.Name)).ToList();
   Expression methodCall;
   if (mtd.IsStatic) methodCall = Expression.Call(null, mtd, prams);
   else {   // on instance-Methods the ownerInstance must be included
      var ownerInstance = Expression.Variable(mtd.DeclaringType, "ownerInstance");
      methodCall = Expression.Call(ownerInstance, mtd, prams);
      prams.Insert(0, ownerInstance);
   }
   return Expression.Lambda(methodCall, false, prams).Compile();
}   

Then Benchmarks

TestClass Foo, with 2 Methods (void and int):

C#
class Foo {
   public void Nop() { }
   public int Mult3(int x) { return x * 3; }
}

Benchmark-Code: First empty run, then call Foo.Nop() / Foo.Mult3() 10000000 times, applying three different kinds of call:

  • DelegateExec - call by hardcoded anonymous method
  • MethodInfoExec - generic call by MethodInfo.Invoke()
  • CompiledInfExec - generic call by  anonymous method, which was built by compiling the MethodInfo
C#
const int _LoopCount = 9999999;
Type _tpFoo = typeof(Foo);
for (var i = _LoopCount; i-- > 0; ) ;                                                                // EmptyRun - #0

var anonymous = (Func<Foo, int, int>)((foo, x) => foo.Mult3(x));
for (var i = _LoopCount; i-- > 0; ) { var y = anonymous(_Foo, 3); }                          // DelegateExec Mult3 #1

var inf = _tpFoo.GetMethod("Mult3");			// MethodInfo of Mult3()

for (var i = _LoopCount; i-- > 0; ) { var y = (int)inf.Invoke(_Foo, new object[] { 3 }); }  // MethodInfoExec Mult3 #2

anonymous = inf.MakeCompiledMethod<Func<Foo, int, int>>();   // inf compiled
for (var i = _LoopCount; i-- > 0; ) { var y = anonymous(_Foo, 3); }                        // CompiledInfExec Mult3 #3

var anonymous = (Action<Foo>)(foo => foo.Nop());
for (var i = _LoopCount; i-- > 0; ) anonymous(_Foo);                                            // DelegateExec Nop #4

var inf = _tpFoo.GetMethod("Nop");			// MethodInfo of Nop() 

for (var i = _LoopCount; i-- > 0; ) inf.Invoke(_Foo, null);                                   // MethodInfoExec Nop #5

anonymous = inf.MakeCompiledMethod<Action<Foo>>();   // inf compiled
for (var i = _LoopCount; i-- > 0; ) anonymous(_Foo);                                         // CompiledInfExec Nop #6

Benchmark-Results: Milliseconds, to execute Foo.Nop() / Foo.Mult3() 10000000 times:

#0 - Executing EmptyRun: 48
#1 - Executing DelegateExec    Mult3:  432
#2 - Executing MethodInfoExec  Mult3: 5706
#3 - Executing CompiledInfExec Mult3:  419
#4 - Executing DelegateExec    Nop:  153
#5 - Executing MethodInfoExec  Nop: 3030
#6 - Executing CompiledInfExec Nop:  275

You see: Using Reflection - "MethodInfoExec" is the slowest. Doing it with compiled MethodInfo - "CompiledInfExec" - is 10 times faster.
Applied on the Method with return-value "CompiledInfExec" is even faster (a very little bit) than the hardcoded anonymous method - "DelegateExec"! - I have no idea why.

(Here I could finish, recommend the attached sources, and it wouldn't be the worst codeproject-tip of all, I think)

System.Linq.Expressions

But let me take the opportunity to give a short, simplyfied and incorrect introduction to the used technology - namely the System.Linq.Expressions.Expression-Class and its Derivates.
Together it is a System, which models a complete Programming-Language - maybe it's the Common Intermediate Language (CIL) itself, which is modelled.

Just to get an imagination of the provided power, see some of the (method-)names:

Add(); AddAssign(); And(); AndAlso(); ArrayAccess(); ArrayIndex(); ArrayLength(); Assign(); Block(); Break(); Call(); Catch(); 
ClearDebugInfo(); Coalesce(); Condition(); Constant(); Continue(); Convert(); DebugInfo(); Decrement(); Default(); Divide(); DivideAssign(); 
Dynamic(); ElementInit(); Empty(); Equal(); ExclusiveOr(); Field(); Goto(); GreaterThan(); GreaterThanOrEqual(); IfThen(); IfThenElse(); 
Increment(); Invoke(); IsFalse(); IsTrue(); Lambda(); LeftShift(); LessThan(); LessThanOrEqual(); ListInit(); Loop(); MakeBinary(); 
MakeCatchBlock(); MakeDynamic(); MakeGoto(); MakeIndex(); MakeMemberAccess(); MakeTry(); MakeUnary(); Modulo(); Multiply(); Negate();
ewArrayBounds(); Not(); NotEqual(); Or(); OrElse(); Parameter(); PostIncrementAssign(); Power(); Property(); PropertyOrField(); 
ReferenceEqual(); ReferenceNotEqual(); Rethrow(); Return(); RightShift(); Subtract(); Switch(); Throw(); TryCatch(); TypeAs(); TypeEqual(); 
TypeIs(); UnaryPlus(); Variable(); 

Surely you recognize equivalences for almost all c#-Keywords and Operators - that is not by accident.

Don't worry: You don't have to handle all of this, or even understand it - most of you will never deal with anything of it.

But in some particular cases it can be useful to pick a small piece of it and make it work.
For instance my "Expression-Exercise" (see the "Code First" - Listing) takes information from an arbitrary MethodInfo to build an anonymous method which calls the by the MethodInfo described Method, but without Reflection, means: fast.
As the Benchmark shows, it may be still 2 times slower than a hard-coded anonymous method, but is about 10 times faster than doing it with Reflection.

Some points of Interest on Expressions

  • Tree-Structure
    Expression (and Derivate)-Instances are designed to be composed together to complex Trees of arbitrary Size - the Expression-Tree
  • No Constructor
    Expression-Instances can't be created with new-Keyword. Instead of that use one the many static generator-Methods - as listed above (and there are some more, and lots of overloads).
  • Building an Expression-Tree is slow
    It takes some milliseconds, especially compiling. What is fast is using the result-Delegate. So for just one or two method-calls this approach is very inappropriate - better stay with Reflection.

the Code in Detail

The first listing is only half of the truth. Its drawback is, that it returns an arbitrary anonymous method of Type Delegate - which is abstract, and not useable for anything. You must cast it to a concrete Delegate-Type, like Action<T>, Func<T1, TResult> -  or whatever.
The Problem is evident: Without knowing the concrete signature you can't call a method (ok - you can - but not with success).
For that I wrapped the core-method into another, which only cares about the casting from abstract Delegate to concrete, and especially ensures a meaningfull InvalidCastException on failure.
Believe it or not: That wrapper-Method was harder to develop than the wrapped core - see all together:

C#
  1  public static Delegate MakeCompiledMethod(this MethodInfo mtd) {
  2     if (mtd == null) throw new ArgumentNullException("ReflectionX.MakeCompiledMethod(MethodInfo mtd): mtd mustn't be null");
  3     var prams = mtd.GetParameters().Select(p => Expression.Parameter(p.ParameterType, p.Name)).ToList();
  4     Expression methodCall;
  5     if (mtd.IsStatic) methodCall = Expression.Call(instance: null, method: mtd, arguments: prams);
  6     else {   // on instance-Methods the ownerInstance must be included
  7        var ownerInstance = Expression.Variable(mtd.DeclaringType, "ownerInstance");
  8        methodCall = Expression.Call(ownerInstance, mtd, prams);
  9        prams.Insert(0, ownerInstance);
 10     }
 11     return Expression.Lambda(methodCall, false, prams).Compile();
 12  }
 13  /// <summary> generates an anonymous Method to call the MethodInfo-method nearly as fast as direct calls. 
 14  /// Eg an (Instance-) MethodInfo "bool StringCollection.Contains(<string>)" would compile to "Func<StringCollection, string, bool>".
 15  /// On the other hand "static bool String.IsNullOrEmpty(<string>)" would compile to "Func<string, bool>".
 16  /// You must specify the correct Delegate-Typparam T to make this method work.</summary>
 17  public static T MakeCompiledMethod<T>(this MethodInfo mtd) {
 18     object dlg = mtd.MakeCompiledMethod();
 19     try { return (T)dlg; }
 20     catch (InvalidCastException x) {//build a proper Exception is more difficult than the function itself
 21        var stpParams = string.Join(", ", mtd.GetParameters().Select(p => p.ParameterType.FriendlyName()));
 22        var sMtd = string.Format("{0} {1}.{2}({3})", mtd.ReturnType.FriendlyName(), mtd.DeclaringType.FriendlyName(), mtd.Name, stpParams);
 23        var sIsStatic = mtd.IsStatic ? "static " : "";
 24        var msg = @"The given MethodInfo ""{0}{1}"" would compile to ""{2}"", but my TypeParameter requests ""{3}""";
 25        msg = string.Format(msg, sIsStatic, sMtd, dlg.GetType().FriendlyName(), typeof(T).FriendlyName());
 26        throw new InvalidOperationException("ReflectionX.MakeCompiledMethod<T>(MethodInfo mtd): " + msg, x);
 27     }
 28  }
 29  /// <summary> a helpful helper </summary>
 30  public static string FriendlyName(this Type tp) {
 31     var rgx = new Regex(@"(?<=(^|[\.\+]))\b\D\w+($|[,`])|\[\[|\]\]");
 32     return string.Concat(rgx.Matches(tp.FullName).Cast<Match>()).Replace("`[[", "<").Replace(",]]", ">").Replace(",", ", ");
 33  }

But I only explain the core (lines #1 - #12) - as an "Exercise in Linq.Expressions" - to recognize what is said in the abovementioned Points of Interest.

#3: get the Method-Parameters and convert them to a List of ParameterExpression
#4: the methodCall-Expression must be pre-declared - because there are two different options to build it:
#5: on static Methods the first argument - "instance" must be null
#7: on instance-Methods we need an additional expression for qualifying the ownerInstance of the instance-Method
#8: pass the arguments to the .Call-call
#9: Our prams-Param-List will be reused when building the compilable Lambda-Expression. Since instance-Methods require as additional Information the ownerInstance, we have to insert it at first position of our params-List.
#11: Create a Lambda-Expression (whatever that means - but it is compilable), compile and return it.

You see: We have built a little tree:
Root is the Lambda, it contains a methodCall-Expression and a List of ParameterExpressions
The methodCall contains a Variable-Expression as Instance, and some Param-Expressions

Lambda - pram1, pram2, ...
      \
       MethodCall - pram2, pram3, ...
                 \
                  ownerInstance (==pram1)

Propably that will be more clear, when you inspect the attached sample-code, using breakpoints and stuff.

Conclusion

My initial goal was simply to share my fancy MethodInfo-Compiler. But then I thought that it doesn't harm, to get at least an idea of the technology behind.
To give an "Entry-Point", if you want develop your own solutions for other, similar problems.

For instance you can take my "MethodInfo-Compiler" as a kind of Template, to develop something similar for Reflection.FieldInfo, or .PropertyInfo

Please note, that the core-method is very stable: Whenever there is a valid MethodInfo (and invalid MethodInfos do not exist) - then it contains all needed data, to create a valid Linq.Expressions from it.
Sorry - minor modification: Presence of ref- or out- Parameters may cause trouble (but laziness prevents me from checking it out in detail).
But what can be said reliable: MethodInfos of Methods with signature of any kind of Action or Func always can be compiled successfully.

An additional Point of Interest may be, that also private Class-Members are accessible by Reflection - and with help of Linq.Expressions that can be well performant.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionCreateDelegate is faster. Pin
Dismember27-Oct-17 22:20
Dismember27-Oct-17 22:20 
AnswerRe: CreateDelegate is faster. Pin
Mr.PoorEnglish28-Oct-17 2:09
Mr.PoorEnglish28-Oct-17 2:09 
GeneralRe: CreateDelegate is faster. Pin
asiwel30-Oct-17 20:03
professionalasiwel30-Oct-17 20:03 
GeneralCode Interesting even if Description Difficult to Follow Pin
kburgoyne127-Oct-17 14:10
kburgoyne127-Oct-17 14:10 
General[My vote of 1] Incoherent Pin
asiwel27-Oct-17 7:30
professionalasiwel27-Oct-17 7:30 
GeneralRe: [My vote of 1] Incoherent Pin
Mr.PoorEnglish27-Oct-17 9:28
Mr.PoorEnglish27-Oct-17 9:28 
GeneralRe: [My vote of 1] Incoherent - going to change that to a 4 Pin
asiwel27-Oct-17 10:39
professionalasiwel27-Oct-17 10:39 
GeneralRe: [My vote of 1] Incoherent - going to change that to a 4 Pin
Mr.PoorEnglish27-Oct-17 11:00
Mr.PoorEnglish27-Oct-17 11:00 
GeneralMy vote of 5 Pin
L Hills27-Oct-17 5:07
L Hills27-Oct-17 5:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.