Click here to Skip to main content
15,889,808 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 
Now that it is later in the day and given your courteous reply, I think an apology on my part is in order here and I will try to change my vote.

For some odd reason, I reacted badly to "Mr.PoorEnglish" which I thought was not an excuse for not clarifying the code throughout the article in much more detail. I apologize for that ... As a matter of fact, I was once fairly fluent in German, having studied it many years at home, at the Goethe Institute in Rothenburg, odT, and living 3 years in Pirmasens, Germany. But today I would sure say that I am "Mr.PoorDeustch" myself (which is certainly why I am not replying in German! Smile | :) ).

Furthermore I misread/misunderstood the name of the download ... which turned out to be an ordinary C# source code solution. An "entry point" into understanding the code, NOT simply having an "entry point" into a compiled library. For that remark, I owe a second apology.

Thirdly, I apologize for that "article with an attitude" remark. That might be considered an "American colloquialism" for a news report or a political statement that has a clear "bias" of some sort or one that somehow insults some readers. Your article is perfectly fine, especially so on re-reading (and others voted it "5"). The only "attitude" there, it seems, is about the fun of coding cool stuff! Smile | :)

It would help, I believe, to begin your articles with a short narrative explanation of the goal and the process proposed to achieve it. "Code First" - particularly specialized code calling functions that have not been explained, etc. - left me wondering "What is this?" I did look carefully at your "Code in Detail" section but found (and still find) it rather obtuse and in need of much further clarification. Perhaps that is just a way of saying I didn't fully understand it or its purpose simply by reading it (even with the several one-sentence clarification points you provided.)

And, yes, you did say "... mostly is not needed. Only may be in "some particular cases" ... But this morning, on my first read, that left me thinking "Why am I reading this, then? What is the advantage of all this complex coding? Why write such an article anyway?" So, again, another apology for that (my own bad attitude!). Clearly there are "use cases" where speed gains like this are important and when they are, your code shows how to implement a much faster approach.

Good wishes and keep on contributing, "Mr.PoorEnglish." I will now try to figure out how to change my vote (which I do not think I have ever needed to do before).
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.