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/