Search code examples
c#performancelambdaexpressionvirtual-functions

Performance of Expression.Compile vs Lambda, direct vs virtual calls


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);

Solution

  • 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/