Performance of Expression.Compile vs Lambda, direct vs virtual calls
Asked Answered
S

4

39

I'm curious how performant the Expression.Compile is versus lambda expression in the code and versus direct method usage, and also direct method calls vs virtual method calls (pseudo code):

var foo = new Foo();
var iFoo = (IFoo)foo;

foo.Bar();
iFoo.Bar();
(() => foo.Bar())();
(() => iFoo.Bar())();
Expression.Compile(foo, Foo.Bar)();
Expression.Compile(iFoo, IFoo.Bar)();
Expression.CompileToMethod(foo, Foo.Bar);
Expression.CompileToMethod(iFoo, IFoo.Bar);
MethodInfo.Invoke(foo, Foo.Bar);
MethodInfo.Invoke(iFoo, IFoo.Bar);
Sabbatarian answered 4/3, 2016 at 20:39 Comment(1)
What do you mean by "how good"? Are you looking for execution performance?Headstock
N
10

Updated measurements:

Updated results for running on .NET 5.0 and with added FastExpressionCompiler library (.CompileFast() rows):

R Call Invocation type ms
1 Virtual IFoo.Bar() 434
1 Direct Foo.Bar() 324
4-6 Virtual (iFooArg) => iFooArg.Bar() 597
4-6 Direct (fooArg) => fooArg.Bar() 487
4-6 Virtual () => IFoo.Bar() 596
4-6 Direct () => FooImpl.Bar() 487
2-3 Virtual Manual Func<IFoo, int> Expression + .Compile() 595
2-3 Direct Manual Func<FooImpl, int> Expression + .Compile() 433
2-3 Virtual CSharpScript.Eval. Func<IFoo, int> expr + .Compile() 594
2-3 Direct CSharpScript.Eval. Func<FooImpl, int> expr + .Compile() 433
9 Virtual Manual Func<int> Expression + .Compile() 866
9 Direct Manual Func<int> Expression + .Compile() 542
4-6 Virtual Manual Func<IFoo, int> Expression + .CompileFast() 596
4-6 Direct Manual Func<FooImpl, int> Expression + .CompileFast() 485
7-8 Virtual CSharpScript.Eval. Func<IFoo, int> expr + .CompileFast() 649
7-8 Direct CSharpScript.Eval. Func<FooImpl, int> expr + .CompileFast() 486
7-8 Virtual Manual Func<int> Expression + .CompileFast() 650
7-8 Direct Manual Func<int> Expression + .CompileFast() 486
10 Virtual CSharpScript.Eval. Func<IFoo, int> to lambda 810
10 Direct CSharpScript.Eval. Func<FooImpl, int> to lambda 758
99 Virtual MethodInfo.Invoke(FooImpl, Bar) 38529
99 Direct MethodInfo.Invoke(IFoo, Bar) 19380

Rows are grouped by invocation types, ranked by total time of direct-call variant (and by virtual-call is special cases).

Note that:

  • pre-compiled direct-call lambdas seem to have no performance difference w.r.t. way of accessing the instance reference (from closure / from argument); since the instance likely isn't accessed at all in the resulting assembly (it is not needed), it makes sense.
  • Expression.Compile with hand-written expression is faster then pre-compiled lambda, but only when instance reference is passed as argument! When the instance reference is stored as a constant (simulating closure), it is slower then pre-compiled lambdas!
  • Using CSharpScript.EvaluateAsync to generate the expression of instance-ref-by-argument-call and then compiling with Expression.Compile is comparable to writing the expression by hand (and compiling it). Performance might vary with library/compiler version!
  • Using CSharpScript.EvaluateAsync to generate a lambda directly is slower then generating an expression and then compiling with Expression.Compile
  • time it takes to actually compile the expressions/script to lambdas is not benchmarked

Original:

I sligthly modified the code of @Serge Semenov and ran it on .NET Core 3.1 - it seems the performance of Expression.Compile() has changed dramatically. I have also added code that uses CSharpScript to compile lambdas from string. Note that .CompileToMethod is not available in .NET Core.

R Call Invocation type ms
1 Virtual IFoo.Bar() 431
1 Direct Foo.Bar() 319
4 Virtual (iFooArg) => iFooArg.Bar() 622
4 Direct (fooArg) => fooArg.Bar() 478
5 Virtual () => IFoo.Bar() 640
5 Direct () => FooImpl.Bar() 477
2 Virtual Manual Func<IFoo, int> Expression + .Compile() 531
2 Direct Manual Func<FooImpl, int> Expression + .Compile() 426
3 Virtual CSharpScript.Eval. Func<IFoo, int> expr + Expression.Compile() 586
3 Direct CSharpScript.Eval. Func<FooImpl, int> expr + Expression.Compile() 423
6 Virtual Manual Func<int> Expression + .Compile() 908
6 Direct Manual Func<int> Expression + .Compile() 584
7 Virtual CSharpScript.Eval. Func<IFoo, int> to lambda 799
7 Direct CSharpScript.Eval. Func<FooImpl, int> to lambda 748
99 Virtual MethodInfo.Invoke(FooImpl, Bar) 43533
99 Direct MethodInfo.Invoke(IFoo, Bar) 29012

Rows are grouped by invocation types, ranked by total time of direct-call variant (and by virtual-call variant is difference is not significant enough).

Code:

//#define NET_FW    //if you run this on .NET Framework and not .NET Core or .NET (5+)

//uses:
// FastExpressionCompiler 3.3.4
// Microsoft.CodeAnalysis.CSharp.Scripting

using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using FastExpressionCompiler;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

namespace ExpressionTest
{
   public interface IFoo
   {
      int Bar();
   }

   public sealed class FooImpl : IFoo
   {
      [MethodImpl(MethodImplOptions.NoInlining)]
      public int Bar()
      {
         return 0;
      }
   }

   class Program
   {
      static void Main(string[] args)
      {
         var foo = new FooImpl();
         var iFoo = (IFoo)foo;

         Func<int> directLambda = () => foo.Bar();
         Func<int> virtualLambda = () => iFoo.Bar();
         Func<FooImpl, int> directArgLambda = fooArg => fooArg.Bar();
         Func<IFoo, int> virtualArgLambda = iFooArg => iFooArg.Bar();

         var compiledArgDirectCall = CompileBar<FooImpl>();
         var compiledArgVirtualCall = CompileBar<IFoo>();
         var compiledArgFromScriptDirectCall = CompileBarFromExprFromScript<FooImpl>();
         var compiledArgFromScriptVirtualCall = CompileBarFromExprFromScript<IFoo>();
         var compiledDirectCall = CompileBar(foo, asInterfaceCall: false);
         var compiledVirtualCall = CompileBar(foo, asInterfaceCall: true);

         var compiledFastArgDirectCall = CompileFastBar<FooImpl>();
         var compiledFastArgVirtualCall = CompileFastBar<IFoo>();
         var compiledFastArgFromScriptDirectCall = CompileFastBarFromExprFromScript<FooImpl>();
         var compiledFastArgFromScriptVirtualCall = CompileFastBarFromExprFromScript<IFoo>();
         var compiledFastDirectCall = CompileFastBar(foo, asInterfaceCall: false);
         var compiledFastVirtualCall = CompileFastBar(foo, asInterfaceCall: true);

         var barMethodInfo = typeof(FooImpl).GetMethod(nameof(FooImpl.Bar));
         var iBarMethodInfo = typeof(IFoo).GetMethod(nameof(IFoo.Bar));
#if NET_FW
         var compiledToModuleDirect = CompileToModule<FooImpl>();
         var compiledToModuleVirtual = CompileToModule<IFoo>();
#endif
         var compiledViaScriptDirect = CompileViaScript<FooImpl>();
         var compiledViaScriptVirtual = CompileViaScript<IFoo>();


         var iterationCount = 0;

         int round = 0;
         start:
         if (round == 0)
         {
            iterationCount = 2000000;
            Console.WriteLine($"Burn in");
            Console.WriteLine($"Iteration count: {iterationCount:N0}");
            goto doWork;
         }
         if (round == 1)
         {
            Task.Delay(5000).Wait();
            iterationCount = 200000000;
            Console.WriteLine($"Iteration count: {iterationCount:N0}");
            goto doWork;
         }
         return;

         doWork:
         {
            Stopwatch sw;
            long elapsedMs;

            sw = Stopwatch.StartNew();
            Console.WriteLine($"Call | Invocation type | ms");
            Console.WriteLine($"-----|-----------------|--:");

            for (int i = 0; i < iterationCount; i++)
               iFoo.Bar();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               iFoo.Bar();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `IFoo.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               foo.Bar();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               foo.Bar();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Foo.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               virtualArgLambda(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               virtualArgLambda(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `(iFooArg) => iFooArg.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               directArgLambda(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               directArgLambda(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `(fooArg)  => fooArg.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               virtualLambda();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               virtualLambda();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `() => IFoo.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               directLambda();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               directLambda();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `() => FooImpl.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledArgVirtualCall(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledArgVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<IFoo, int>    Expression + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledArgDirectCall(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledArgDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual Func<FooImpl, int> Expression + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledArgFromScriptVirtualCall(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledArgFromScriptVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `CSharpScript.Eval. Func<IFoo, int> expr    + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledArgFromScriptDirectCall(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledArgFromScriptDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `CSharpScript.Eval. Func<FooImpl, int> expr + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledVirtualCall();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledVirtualCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<int> Expression + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledDirectCall();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledDirectCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual Func<int> Expression + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastArgVirtualCall(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastArgVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<IFoo, int>    Expression + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastArgDirectCall(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastArgDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual Func<FooImpl, int> Expression + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastArgFromScriptVirtualCall(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastArgFromScriptVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `CSharpScript.Eval. Func<IFoo, int> expr    + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastArgFromScriptDirectCall(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastArgFromScriptDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `CSharpScript.Eval. Func<FooImpl, int> expr + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastVirtualCall();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastVirtualCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<int> Expression + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastDirectCall();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastDirectCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual Func<int> Expression + .CompileFast()` | {elapsedMs}");

#if NET_FW
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledToModuleVirtual(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<IFoo, int>      Expression + .CompileToMethod()` | {elapsedMs}");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledToModuleDirect(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual (Func<FooImpl, int>) Expression + .CompileToMethod()` | {elapsedMs}");
#endif

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledViaScriptVirtual(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `CSharpScript.Eval. Func<IFoo, int> to lambda` | {elapsedMs}");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledViaScriptDirect(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `CSharpScript.Eval. Func<FooImpl, int> to lambda` | {elapsedMs}");

            //sw.Restart();
            //for (int i = 0; i < iterationCount; i++)
            //{
            //   int result = (int)iBarMethodInfo.Invoke(iFoo, null);
            //}
            //elapsedMs = sw.ElapsedMilliseconds;
            //Console.WriteLine($"Virtual | `MethodInfo.Invoke(FooImpl, Bar)` | {elapsedMs}");

            //sw.Restart();
            //for (int i = 0; i < iterationCount; i++)
            //{
            //   int result = (int)barMethodInfo.Invoke(foo, null);
            //}
            //elapsedMs = sw.ElapsedMilliseconds;
            //Console.WriteLine($"Direct  | `MethodInfo.Invoke(IFoo, Bar)` | {elapsedMs}");
         }
         round++;
         goto start;
      }

      static LambdaExpression GenerateBarExprClosure(IFoo foo, bool asInterfaceCall)
      {
         var fooType = asInterfaceCall ? typeof(IFoo) : foo.GetType();
         var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
         var instance = Expression.Constant(foo, fooType);
         var call = Expression.Call(instance, methodInfo);
         var lambda = Expression.Lambda(call);
         return lambda;
      }

      static LambdaExpression GenerateBarExprArg<TInput>()
      {
         var fooType = typeof(TInput);
         var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
         var instance = Expression.Parameter(fooType, "foo");
         var call = Expression.Call(instance, methodInfo);
         var lambda = Expression.Lambda(call, instance);
         return lambda;
      }

      static Func<int> CompileBar(IFoo foo, bool asInterfaceCall)
      {
         var lambda = GenerateBarExprClosure(foo, asInterfaceCall);
         var compiledFunction = (Func<int>)lambda.Compile();
         return compiledFunction;
      }

      static Func<TInput, int> CompileBar<TInput>()
      {
         var lambda = GenerateBarExprArg<TInput>();
         var compiledFunction = (Func<TInput, int>)lambda.Compile();
         return compiledFunction;
      }

      static Func<int> CompileFastBar(IFoo foo, bool asInterfaceCall)
      {
         var lambda = GenerateBarExprClosure(foo, asInterfaceCall);
         var compiledFunction = (Func<int>)lambda.CompileFast(true);
         return compiledFunction;
      }

      static Func<TInput, int> CompileFastBar<TInput>()
      {
         var lambda = GenerateBarExprArg<TInput>();
         var compiledFunction = (Func<TInput, int>)lambda.CompileFast(true);
         return compiledFunction;
      }

#if NET_FW
      static Func<TInput, int> CompileToModule<TInput>()
      {
         var fooType = typeof(TInput);
         var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
         var instance = Expression.Parameter(fooType, "foo");
         var call = Expression.Call(instance, methodInfo);
         var lambda = Expression.Lambda(call, instance);

         var asmName = new AssemblyName(fooType.Name);
         var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
         var moduleBuilder = asmBuilder.DefineDynamicModule(fooType.Name);
         var typeBuilder = moduleBuilder.DefineType(fooType.Name, TypeAttributes.Public);
         var methodBuilder = typeBuilder.DefineMethod(nameof(IFoo.Bar), MethodAttributes.Static, typeof(int), new[] { fooType });
         Expression.Lambda<Action>(lambda).CompileToMethod(methodBuilder);
         var createdType = typeBuilder.CreateType();

         var mi = createdType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)[1];
         var func = Delegate.CreateDelegate(typeof(Func<TInput, int>), mi);
         return (Func<TInput, int>)func;
      }
#endif

      static Func<TInput, int> CompileViaScript<TInput>()
      {
         ScriptOptions scriptOptions = ScriptOptions.Default;

         //Add reference to mscorlib
         var mscorlib = typeof(System.Object).Assembly;
         var systemCore = typeof(System.Func<>).Assembly;
         var thisAssembly = typeof(IFoo).Assembly;
         scriptOptions = scriptOptions.AddReferences(mscorlib, systemCore, thisAssembly);

         var result = CSharpScript.EvaluateAsync<Func<TInput, int>>("it => it.Bar()", options: scriptOptions).Result;
         return result;
      }
      static Expression<Func<TInput, int>> GenerateExprFromScript<TInput>()
      {
         ScriptOptions scriptOptions = ScriptOptions.Default;

         //Add reference to mscorlib
         var mscorlib = typeof(System.Object).Assembly;
         var systemCore = typeof(System.Func<>).Assembly;
         var thisAssembly = typeof(IFoo).Assembly;
         scriptOptions = scriptOptions.AddReferences(mscorlib, systemCore, thisAssembly);

         var result = CSharpScript.EvaluateAsync<Expression<Func<TInput, int>>>("it => it.Bar()", options: scriptOptions).Result;
         return result;
      }

      static Func<TInput, int> CompileBarFromExprFromScript<TInput>()
      {
         var lambda = GenerateExprFromScript<TInput>();
         var compiledFunction = (Func<TInput, int>)lambda.Compile();
         return compiledFunction;
      }

      static Func<TInput, int> CompileFastBarFromExprFromScript<TInput>()
      {
         var lambda = GenerateExprFromScript<TInput>();
         var compiledFunction = (Func<TInput, int>)lambda.CompileFast(true);
         return compiledFunction;
      }
   }
}

How to use CSharpScript:
https://joshvarty.com/2015/10/15/learn-roslyn-now-part-14-intro-to-the-scripting-api/
https://www.strathweb.com/2018/01/easy-way-to-create-a-c-lambda-expression-from-a-string-with-roslyn/

Napoleonnapoleonic answered 1/7, 2020 at 15:15 Comment(4)
Appreciate the effort, @Artex4964. I marked your answer as the right one since it represents the most recent and relevant data. And I like that that you added the CSharpScript option.Sabbatarian
Thanks... what caught my eye was the parameterized compiled Expression<> being faster then any of the compile-time-generated lambdas. I wonder why that is. The test setup was nowhere perfect (time varying up to 50ms between test), but this fact was consistent among attempts.Napoleonnapoleonic
Anyone interested in the topic should probably benchmark Expression.Compile on their own, if their platform is different. And, maybe also try: github.com/dadhi/FastExpressionCompilerNapoleonnapoleonic
there is one other way I know: Delegate.CreateDelegate(). I hope you add it to the comparison. It's important because in some cases I can not directly call a method. for example in calling a constructor or calling a private method using reflection!Specie
S
46

I didn't find any answer, so here is the performance test:

using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;

namespace ExpressionTest
{
    public interface IFoo
    {
        int Bar();
    }

    public sealed class FooImpl : IFoo
    {
        public int Bar()
        {
            return 0;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var foo = new FooImpl();
            var iFoo = (IFoo)foo;

            Func<int> directLambda = () => foo.Bar();
            Func<int> virtualLambda = () => iFoo.Bar();
            var compiledDirectCall = CompileBar(foo, asInterfaceCall: false);
            var compiledVirtualCall = CompileBar(foo, asInterfaceCall: true);
            var compiledArgDirectCall = CompileBar<FooImpl>();
            var compiledArgVirtualCall = CompileBar<IFoo>();
            var barMethodInfo = typeof(FooImpl).GetMethod(nameof(FooImpl.Bar));
            var iBarMethodInfo = typeof(IFoo).GetMethod(nameof(IFoo.Bar));
            var compiledToModuleDirect = CompileToModule<FooImpl>();
            var compiledToModuleVirtual = CompileToModule<IFoo>();

            var iterationCount = 200000000;
            Console.WriteLine($"Iteration count: {iterationCount:N0}");

            var sw = Stopwatch.StartNew();
            for (int i = 0; i < iterationCount; i++)
                compiledVirtualCall();
            var elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual (Func<int>)Expression.Compile(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledDirectCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct (Func<int>)Expression.Compile(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledArgVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual (Func<IFoo, int>)Expression.Compile(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledArgDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct (Func<FooImpl, int>)Expression.Compile(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledToModuleVirtual(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual (Func<IFoo, int>)Expression.CompileToMethod(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                compiledToModuleDirect(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct (Func<FooImpl, int>)Expression.CompileToMethod(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                virtualLambda();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual () => IFoo.Bar(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                directLambda();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct () => FooImpl.Bar(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                iFoo.Bar();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual IFoo.Bar(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
                foo.Bar();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct Foo.Bar(): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++) {
                int result = (int)iBarMethodInfo.Invoke(iFoo, null);
            }
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual MethodInfo.Invoke(FooImpl, Bar): {elapsedMs} ms");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++) {
                int result = (int)barMethodInfo.Invoke(foo, null);
            }
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct MethodInfo.Invoke(IFoo, Bar): {elapsedMs} ms");
        }

        static Func<int> CompileBar(IFoo foo, bool asInterfaceCall)
        {
            var fooType = asInterfaceCall ? typeof(IFoo) : foo.GetType();
            var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
            var instance = Expression.Constant(foo, fooType);
            var call = Expression.Call(instance, methodInfo);
            var lambda = Expression.Lambda(call);
            var compiledFunction = (Func<int>)lambda.Compile();
            return compiledFunction;
        }

        static Func<TInput, int> CompileBar<TInput>()
        {
            var fooType = typeof(TInput);
            var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
            var instance = Expression.Parameter(fooType, "foo");
            var call = Expression.Call(instance, methodInfo);
            var lambda = Expression.Lambda(call, instance);
            var compiledFunction = (Func<TInput, int>)lambda.Compile();
            return compiledFunction;
        }

        static Func<TInput, int> CompileToModule<TInput>()
        {
            var fooType = typeof(TInput);
            var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
            var instance = Expression.Parameter(fooType, "foo");
            var call = Expression.Call(instance, methodInfo);
            var lambda = Expression.Lambda(call, instance);

            var asmName = new AssemblyName(fooType.Name);
            var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
            var moduleBuilder = asmBuilder.DefineDynamicModule(fooType.Name);
            var typeBuilder = moduleBuilder.DefineType(fooType.Name, TypeAttributes.Public);
            var methodBuilder = typeBuilder.DefineMethod(nameof(IFoo.Bar), MethodAttributes.Static, typeof(int), new[] { fooType });
            Expression.Lambda<Action>(lambda).CompileToMethod(methodBuilder);
            var createdType = typeBuilder.CreateType();

            var mi = createdType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)[1];
            var func = Delegate.CreateDelegate(typeof(Func<TInput, int>), mi);
            return (Func<TInput, int>)func;
        }
    }
}

On my laptop (Release mode, 64 bit, .NET 4.5.2) it yields:

Iteration count: 200,000,000
Virtual MethodInfo.Invoke(FooImpl, Bar):               61811 ms
Direct MethodInfo.Invoke(IFoo, Bar):                   37078 ms
Virtual (Func<int>)Expression.Compile():                2894 ms
Direct (Func<int>)Expression.Compile():                 2242 ms
Virtual (Func<IFoo, int>)Expression.Compile():          2319 ms
Direct (Func<FooImpl, int>)Expression.Compile():        2051 ms
Virtual (Func<IFoo, int>)Expression.CompileToMethod():   996 ms
Direct (Func<FooImpl, int>)Expression.CompileToMethod(): 679 ms
Virtual () => IFoo.Bar():                                796 ms
Direct () => FooImpl.Bar():                              469 ms
Virtual IFoo.Bar():                                      531 ms
Direct Foo.Bar():                                         68 ms

Hope this helps.

Sabbatarian answered 4/3, 2016 at 20:39 Comment(2)
but if direct can't be inlined it would make a difference.Kilauea
@Serge Semenov: Thank you for taking the time to test and publishing the results. It is worth mentioning CreateDelegate. I have found that it about as fast as or faster than CompileToMethod and the code is way simpler: var func = (Func<T, int>)Delegate.CreateDelegate(typeof(Func<T, int>), null, methodInfo);Loop
N
10

Updated measurements:

Updated results for running on .NET 5.0 and with added FastExpressionCompiler library (.CompileFast() rows):

R Call Invocation type ms
1 Virtual IFoo.Bar() 434
1 Direct Foo.Bar() 324
4-6 Virtual (iFooArg) => iFooArg.Bar() 597
4-6 Direct (fooArg) => fooArg.Bar() 487
4-6 Virtual () => IFoo.Bar() 596
4-6 Direct () => FooImpl.Bar() 487
2-3 Virtual Manual Func<IFoo, int> Expression + .Compile() 595
2-3 Direct Manual Func<FooImpl, int> Expression + .Compile() 433
2-3 Virtual CSharpScript.Eval. Func<IFoo, int> expr + .Compile() 594
2-3 Direct CSharpScript.Eval. Func<FooImpl, int> expr + .Compile() 433
9 Virtual Manual Func<int> Expression + .Compile() 866
9 Direct Manual Func<int> Expression + .Compile() 542
4-6 Virtual Manual Func<IFoo, int> Expression + .CompileFast() 596
4-6 Direct Manual Func<FooImpl, int> Expression + .CompileFast() 485
7-8 Virtual CSharpScript.Eval. Func<IFoo, int> expr + .CompileFast() 649
7-8 Direct CSharpScript.Eval. Func<FooImpl, int> expr + .CompileFast() 486
7-8 Virtual Manual Func<int> Expression + .CompileFast() 650
7-8 Direct Manual Func<int> Expression + .CompileFast() 486
10 Virtual CSharpScript.Eval. Func<IFoo, int> to lambda 810
10 Direct CSharpScript.Eval. Func<FooImpl, int> to lambda 758
99 Virtual MethodInfo.Invoke(FooImpl, Bar) 38529
99 Direct MethodInfo.Invoke(IFoo, Bar) 19380

Rows are grouped by invocation types, ranked by total time of direct-call variant (and by virtual-call is special cases).

Note that:

  • pre-compiled direct-call lambdas seem to have no performance difference w.r.t. way of accessing the instance reference (from closure / from argument); since the instance likely isn't accessed at all in the resulting assembly (it is not needed), it makes sense.
  • Expression.Compile with hand-written expression is faster then pre-compiled lambda, but only when instance reference is passed as argument! When the instance reference is stored as a constant (simulating closure), it is slower then pre-compiled lambdas!
  • Using CSharpScript.EvaluateAsync to generate the expression of instance-ref-by-argument-call and then compiling with Expression.Compile is comparable to writing the expression by hand (and compiling it). Performance might vary with library/compiler version!
  • Using CSharpScript.EvaluateAsync to generate a lambda directly is slower then generating an expression and then compiling with Expression.Compile
  • time it takes to actually compile the expressions/script to lambdas is not benchmarked

Original:

I sligthly modified the code of @Serge Semenov and ran it on .NET Core 3.1 - it seems the performance of Expression.Compile() has changed dramatically. I have also added code that uses CSharpScript to compile lambdas from string. Note that .CompileToMethod is not available in .NET Core.

R Call Invocation type ms
1 Virtual IFoo.Bar() 431
1 Direct Foo.Bar() 319
4 Virtual (iFooArg) => iFooArg.Bar() 622
4 Direct (fooArg) => fooArg.Bar() 478
5 Virtual () => IFoo.Bar() 640
5 Direct () => FooImpl.Bar() 477
2 Virtual Manual Func<IFoo, int> Expression + .Compile() 531
2 Direct Manual Func<FooImpl, int> Expression + .Compile() 426
3 Virtual CSharpScript.Eval. Func<IFoo, int> expr + Expression.Compile() 586
3 Direct CSharpScript.Eval. Func<FooImpl, int> expr + Expression.Compile() 423
6 Virtual Manual Func<int> Expression + .Compile() 908
6 Direct Manual Func<int> Expression + .Compile() 584
7 Virtual CSharpScript.Eval. Func<IFoo, int> to lambda 799
7 Direct CSharpScript.Eval. Func<FooImpl, int> to lambda 748
99 Virtual MethodInfo.Invoke(FooImpl, Bar) 43533
99 Direct MethodInfo.Invoke(IFoo, Bar) 29012

Rows are grouped by invocation types, ranked by total time of direct-call variant (and by virtual-call variant is difference is not significant enough).

Code:

//#define NET_FW    //if you run this on .NET Framework and not .NET Core or .NET (5+)

//uses:
// FastExpressionCompiler 3.3.4
// Microsoft.CodeAnalysis.CSharp.Scripting

using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using FastExpressionCompiler;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

namespace ExpressionTest
{
   public interface IFoo
   {
      int Bar();
   }

   public sealed class FooImpl : IFoo
   {
      [MethodImpl(MethodImplOptions.NoInlining)]
      public int Bar()
      {
         return 0;
      }
   }

   class Program
   {
      static void Main(string[] args)
      {
         var foo = new FooImpl();
         var iFoo = (IFoo)foo;

         Func<int> directLambda = () => foo.Bar();
         Func<int> virtualLambda = () => iFoo.Bar();
         Func<FooImpl, int> directArgLambda = fooArg => fooArg.Bar();
         Func<IFoo, int> virtualArgLambda = iFooArg => iFooArg.Bar();

         var compiledArgDirectCall = CompileBar<FooImpl>();
         var compiledArgVirtualCall = CompileBar<IFoo>();
         var compiledArgFromScriptDirectCall = CompileBarFromExprFromScript<FooImpl>();
         var compiledArgFromScriptVirtualCall = CompileBarFromExprFromScript<IFoo>();
         var compiledDirectCall = CompileBar(foo, asInterfaceCall: false);
         var compiledVirtualCall = CompileBar(foo, asInterfaceCall: true);

         var compiledFastArgDirectCall = CompileFastBar<FooImpl>();
         var compiledFastArgVirtualCall = CompileFastBar<IFoo>();
         var compiledFastArgFromScriptDirectCall = CompileFastBarFromExprFromScript<FooImpl>();
         var compiledFastArgFromScriptVirtualCall = CompileFastBarFromExprFromScript<IFoo>();
         var compiledFastDirectCall = CompileFastBar(foo, asInterfaceCall: false);
         var compiledFastVirtualCall = CompileFastBar(foo, asInterfaceCall: true);

         var barMethodInfo = typeof(FooImpl).GetMethod(nameof(FooImpl.Bar));
         var iBarMethodInfo = typeof(IFoo).GetMethod(nameof(IFoo.Bar));
#if NET_FW
         var compiledToModuleDirect = CompileToModule<FooImpl>();
         var compiledToModuleVirtual = CompileToModule<IFoo>();
#endif
         var compiledViaScriptDirect = CompileViaScript<FooImpl>();
         var compiledViaScriptVirtual = CompileViaScript<IFoo>();


         var iterationCount = 0;

         int round = 0;
         start:
         if (round == 0)
         {
            iterationCount = 2000000;
            Console.WriteLine($"Burn in");
            Console.WriteLine($"Iteration count: {iterationCount:N0}");
            goto doWork;
         }
         if (round == 1)
         {
            Task.Delay(5000).Wait();
            iterationCount = 200000000;
            Console.WriteLine($"Iteration count: {iterationCount:N0}");
            goto doWork;
         }
         return;

         doWork:
         {
            Stopwatch sw;
            long elapsedMs;

            sw = Stopwatch.StartNew();
            Console.WriteLine($"Call | Invocation type | ms");
            Console.WriteLine($"-----|-----------------|--:");

            for (int i = 0; i < iterationCount; i++)
               iFoo.Bar();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               iFoo.Bar();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `IFoo.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               foo.Bar();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               foo.Bar();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Foo.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               virtualArgLambda(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               virtualArgLambda(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `(iFooArg) => iFooArg.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               directArgLambda(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               directArgLambda(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `(fooArg)  => fooArg.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               virtualLambda();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               virtualLambda();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `() => IFoo.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               directLambda();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               directLambda();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `() => FooImpl.Bar()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledArgVirtualCall(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledArgVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<IFoo, int>    Expression + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledArgDirectCall(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledArgDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual Func<FooImpl, int> Expression + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledArgFromScriptVirtualCall(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledArgFromScriptVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `CSharpScript.Eval. Func<IFoo, int> expr    + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledArgFromScriptDirectCall(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledArgFromScriptDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `CSharpScript.Eval. Func<FooImpl, int> expr + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledVirtualCall();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledVirtualCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<int> Expression + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledDirectCall();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledDirectCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual Func<int> Expression + .Compile()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastArgVirtualCall(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastArgVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<IFoo, int>    Expression + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastArgDirectCall(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastArgDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual Func<FooImpl, int> Expression + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastArgFromScriptVirtualCall(iFoo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastArgFromScriptVirtualCall(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `CSharpScript.Eval. Func<IFoo, int> expr    + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastArgFromScriptDirectCall(foo);
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastArgFromScriptDirectCall(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `CSharpScript.Eval. Func<FooImpl, int> expr + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastVirtualCall();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastVirtualCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<int> Expression + .CompileFast()` | {elapsedMs}");

            for (int i = 0; i < iterationCount; i++)
               compiledFastDirectCall();
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledFastDirectCall();
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual Func<int> Expression + .CompileFast()` | {elapsedMs}");

#if NET_FW
            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledToModuleVirtual(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `Manual Func<IFoo, int>      Expression + .CompileToMethod()` | {elapsedMs}");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledToModuleDirect(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `Manual (Func<FooImpl, int>) Expression + .CompileToMethod()` | {elapsedMs}");
#endif

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledViaScriptVirtual(iFoo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Virtual | `CSharpScript.Eval. Func<IFoo, int> to lambda` | {elapsedMs}");

            sw.Restart();
            for (int i = 0; i < iterationCount; i++)
               compiledViaScriptDirect(foo);
            elapsedMs = sw.ElapsedMilliseconds;
            Console.WriteLine($"Direct  | `CSharpScript.Eval. Func<FooImpl, int> to lambda` | {elapsedMs}");

            //sw.Restart();
            //for (int i = 0; i < iterationCount; i++)
            //{
            //   int result = (int)iBarMethodInfo.Invoke(iFoo, null);
            //}
            //elapsedMs = sw.ElapsedMilliseconds;
            //Console.WriteLine($"Virtual | `MethodInfo.Invoke(FooImpl, Bar)` | {elapsedMs}");

            //sw.Restart();
            //for (int i = 0; i < iterationCount; i++)
            //{
            //   int result = (int)barMethodInfo.Invoke(foo, null);
            //}
            //elapsedMs = sw.ElapsedMilliseconds;
            //Console.WriteLine($"Direct  | `MethodInfo.Invoke(IFoo, Bar)` | {elapsedMs}");
         }
         round++;
         goto start;
      }

      static LambdaExpression GenerateBarExprClosure(IFoo foo, bool asInterfaceCall)
      {
         var fooType = asInterfaceCall ? typeof(IFoo) : foo.GetType();
         var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
         var instance = Expression.Constant(foo, fooType);
         var call = Expression.Call(instance, methodInfo);
         var lambda = Expression.Lambda(call);
         return lambda;
      }

      static LambdaExpression GenerateBarExprArg<TInput>()
      {
         var fooType = typeof(TInput);
         var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
         var instance = Expression.Parameter(fooType, "foo");
         var call = Expression.Call(instance, methodInfo);
         var lambda = Expression.Lambda(call, instance);
         return lambda;
      }

      static Func<int> CompileBar(IFoo foo, bool asInterfaceCall)
      {
         var lambda = GenerateBarExprClosure(foo, asInterfaceCall);
         var compiledFunction = (Func<int>)lambda.Compile();
         return compiledFunction;
      }

      static Func<TInput, int> CompileBar<TInput>()
      {
         var lambda = GenerateBarExprArg<TInput>();
         var compiledFunction = (Func<TInput, int>)lambda.Compile();
         return compiledFunction;
      }

      static Func<int> CompileFastBar(IFoo foo, bool asInterfaceCall)
      {
         var lambda = GenerateBarExprClosure(foo, asInterfaceCall);
         var compiledFunction = (Func<int>)lambda.CompileFast(true);
         return compiledFunction;
      }

      static Func<TInput, int> CompileFastBar<TInput>()
      {
         var lambda = GenerateBarExprArg<TInput>();
         var compiledFunction = (Func<TInput, int>)lambda.CompileFast(true);
         return compiledFunction;
      }

#if NET_FW
      static Func<TInput, int> CompileToModule<TInput>()
      {
         var fooType = typeof(TInput);
         var methodInfo = fooType.GetMethod(nameof(IFoo.Bar));
         var instance = Expression.Parameter(fooType, "foo");
         var call = Expression.Call(instance, methodInfo);
         var lambda = Expression.Lambda(call, instance);

         var asmName = new AssemblyName(fooType.Name);
         var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
         var moduleBuilder = asmBuilder.DefineDynamicModule(fooType.Name);
         var typeBuilder = moduleBuilder.DefineType(fooType.Name, TypeAttributes.Public);
         var methodBuilder = typeBuilder.DefineMethod(nameof(IFoo.Bar), MethodAttributes.Static, typeof(int), new[] { fooType });
         Expression.Lambda<Action>(lambda).CompileToMethod(methodBuilder);
         var createdType = typeBuilder.CreateType();

         var mi = createdType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)[1];
         var func = Delegate.CreateDelegate(typeof(Func<TInput, int>), mi);
         return (Func<TInput, int>)func;
      }
#endif

      static Func<TInput, int> CompileViaScript<TInput>()
      {
         ScriptOptions scriptOptions = ScriptOptions.Default;

         //Add reference to mscorlib
         var mscorlib = typeof(System.Object).Assembly;
         var systemCore = typeof(System.Func<>).Assembly;
         var thisAssembly = typeof(IFoo).Assembly;
         scriptOptions = scriptOptions.AddReferences(mscorlib, systemCore, thisAssembly);

         var result = CSharpScript.EvaluateAsync<Func<TInput, int>>("it => it.Bar()", options: scriptOptions).Result;
         return result;
      }
      static Expression<Func<TInput, int>> GenerateExprFromScript<TInput>()
      {
         ScriptOptions scriptOptions = ScriptOptions.Default;

         //Add reference to mscorlib
         var mscorlib = typeof(System.Object).Assembly;
         var systemCore = typeof(System.Func<>).Assembly;
         var thisAssembly = typeof(IFoo).Assembly;
         scriptOptions = scriptOptions.AddReferences(mscorlib, systemCore, thisAssembly);

         var result = CSharpScript.EvaluateAsync<Expression<Func<TInput, int>>>("it => it.Bar()", options: scriptOptions).Result;
         return result;
      }

      static Func<TInput, int> CompileBarFromExprFromScript<TInput>()
      {
         var lambda = GenerateExprFromScript<TInput>();
         var compiledFunction = (Func<TInput, int>)lambda.Compile();
         return compiledFunction;
      }

      static Func<TInput, int> CompileFastBarFromExprFromScript<TInput>()
      {
         var lambda = GenerateExprFromScript<TInput>();
         var compiledFunction = (Func<TInput, int>)lambda.CompileFast(true);
         return compiledFunction;
      }
   }
}

How to use CSharpScript:
https://joshvarty.com/2015/10/15/learn-roslyn-now-part-14-intro-to-the-scripting-api/
https://www.strathweb.com/2018/01/easy-way-to-create-a-c-lambda-expression-from-a-string-with-roslyn/

Napoleonnapoleonic answered 1/7, 2020 at 15:15 Comment(4)
Appreciate the effort, @Artex4964. I marked your answer as the right one since it represents the most recent and relevant data. And I like that that you added the CSharpScript option.Sabbatarian
Thanks... what caught my eye was the parameterized compiled Expression<> being faster then any of the compile-time-generated lambdas. I wonder why that is. The test setup was nowhere perfect (time varying up to 50ms between test), but this fact was consistent among attempts.Napoleonnapoleonic
Anyone interested in the topic should probably benchmark Expression.Compile on their own, if their platform is different. And, maybe also try: github.com/dadhi/FastExpressionCompilerNapoleonnapoleonic
there is one other way I know: Delegate.CreateDelegate(). I hope you add it to the comparison. It's important because in some cases I can not directly call a method. for example in calling a constructor or calling a private method using reflection!Specie
U
8

We can split a question to 2 cases:

  • how bare .NET work with method calls itself (infrastructure question)?
  • how optimizers assist to method calling?

ExpressionTest.exe in Release mode with optimization (default release settings) .NET 4.5.2:

Compiled Virtual Call: 4625 ms
Compiled Direct Call: 3361 ms
Lambda Virtual Call: 1096 ms
Lambda Direct Call: 576 ms
Virtual Call: 649 ms
Direct Call: 144 ms

We see that "Direct Call" in 4.5 times faster than "Virtual Call". But as we see above it's no call at all. Bar method was inlined.

ExpressionTest.exe in Release mode with no optimization .NET 4.5.2:

Compiled Virtual Call: 5394 ms
Compiled Direct Call: 4666 ms
Lambda Virtual Call: 1800 ms
Lambda Direct Call: 1683 ms
Virtual Call: 1154 ms
Direct Call: 1112 ms

So, "Direct Call" is about 3-4% faster than "Virtual Call".

Similar question: Performance of "direct" virtual call vs. interface call in C#

Uneducated answered 4/3, 2016 at 23:17 Comment(2)
I'm more interested in why Expression.Compile() gives a slower version of the method vs. lambda "() => { ... }" and is there is a way to get an optimized code out of Expression.Compile()?Sabbatarian
I'm also interested why the compiled calls (both to Method and to Delegate) are slower than constructing a new expression tree from the lambda expression on the fly then executing the tree.... I'd also be interested in how much longer "CompileToModule" took to run than "CompileBar" and what the memory usage was.Deranged
U
4

Hint: in release mode no call made at all in "Direct Call" case. CPU going from 00B531BC (mov eax ...) to 00B531C8 (jl 00B531BC) only.

                for (int i = 0; i < iterationCount; i++)
00B531BA  xor         edx,edx  
                foo.Bar();
00B531BC  mov         eax,dword ptr [ebx+4]  // actual loop begin
00B531BF  cmp         byte ptr [eax],al  
            for (int i = 0; i < iterationCount; i++)
00B531C1  inc         edx  
00B531C2  cmp         edx,0BEBC200h // 0BEBC200h = 200000000
00B531C8  jl          00B531BC      // loop begin address
Uneducated answered 4/3, 2016 at 22:39 Comment(1)
That's right, it does inlining, thanks. This should be a comment thoughtSabbatarian

© 2022 - 2024 — McMap. All rights reserved.