Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork91
Fast Compiler for C# Expression Trees and the lightweight LightExpression alternative. Diagnostic and code generation tools for the expressions.
License
dadhi/FastExpressionCompiler
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Targets .NET 6+, .NET 4.7.2+, .NET Standard 2.0+
NuGet packages:
The project was originally a part of theDryIoc, so check it out ;-)
ExpressionTree compilation is used by the wide variety of tools, e.g. IoC/DI containers, Serializers, ORMs and OOMs.ButExpression.Compile()
is just slow.Moreover the compiled delegate may be slower than the manually created delegate because of thereasons:
TL;DR;
Expression.Compile creates a DynamicMethod and associates it with an anonymous assembly to run it in a sand-boxed environment. This makes it safe for a dynamic method to be emitted and executed by partially trusted code but adds some run-time overhead.
See alsoa deep dive to Delegate internals.
The FastExpressionCompiler.CompileFast()
extension method is10-40x times faster than.Compile()
.
The compiled delegate may bein some cases a lot faster than the one produced by.Compile()
.
Note: The actual performance may vary depending on the multiple factors:platform, how complex is expression, does it have a closure, does it contain nested lambdas, etc.
In addition, the memory consumption taken by the compilation will be much smaller (check theAllocated
column in thebenchmarks below).
Updated to .NET 9.0
BenchmarkDotNet v0.15.0, Windows 11 (10.0.26100.4061/24H2/2024Update/HudsonValley)Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores.NET SDK 9.0.203[Host] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2DefaultJob : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2
vara=newA();varb=newB();Expression<Func<X>>e=()=>newX(a,b);
Compiling expression:
Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|
CompileFast | 3.183 us | 0.0459 us | 0.0407 us | 1.00 | 0.02 | 1 | 0.1984 | 0.1945 | 1.23 KB | 1.00 |
Compile | 147.312 us | 1.9291 us | 1.8946 us | 46.28 | 0.81 | 2 | 0.4883 | 0.2441 | 4.48 KB | 3.65 |
Invoking the compiled delegate (comparing to the direct constructor call):
Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|
DirectConstructorCall | 6.055 ns | 0.0632 ns | 0.0560 ns | 1.00 | 0.01 | 1 | 0.0051 | 32 B | 1.00 |
CompiledLambda | 7.853 ns | 0.2013 ns | 0.1681 ns | 1.30 | 0.03 | 2 | 0.0051 | 32 B | 1.00 |
FastCompiledLambda | 7.962 ns | 0.2186 ns | 0.4052 ns | 1.31 | 0.07 | 2 | 0.0051 | 32 B | 1.00 |
vara=newA();varb=newB();Expression<Func<X>>getXExpr=()=>CreateX((aa,bb)=>newX(aa,bb),newLazy<A>(()=>a),b);
Compiling expression:
Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|
CompileFast | 11.12 us | 0.189 us | 0.158 us | 1.00 | 0.02 | 1 | 0.6104 | 0.5798 | 3.77 KB | 1.00 |
Compile | 415.09 us | 4.277 us | 3.571 us | 37.34 | 0.60 | 2 | 1.9531 | 1.4648 | 12.04 KB | 3.19 |
Invoking compiled delegate comparing to direct method call:
Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|
DirectMethodCall | 40.29 ns | 0.549 ns | 0.487 ns | 1.00 | 0.02 | 1 | 0.0268 | 168 B | 1.00 |
Invoke_CompiledFast | 40.59 ns | 0.157 ns | 0.123 ns | 1.01 | 0.01 | 1 | 0.0166 | 104 B | 0.62 |
Invoke_Compiled | 1,142.12 ns | 11.877 ns | 14.586 ns | 28.35 | 0.48 | 2 | 0.0420 | 264 B | 1.57 |
vara=newA();varbParamExpr=Expression.Parameter(typeof(B),"b");varexpr=Expression.Lambda(Expression.New(_ctorX,Expression.Constant(a,typeof(A)),bParamExpr),bParamExpr);
Compiling expression:
Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|
CompileFast_LightExpression | 3.107 us | 0.0562 us | 0.0498 us | 0.99 | 0.02 | 1 | 0.1755 | 0.1678 | 1.08 KB | 1.00 |
CompileFast_SystemExpression | 3.126 us | 0.0288 us | 0.0256 us | 1.00 | 0.01 | 1 | 0.1755 | 0.1678 | 1.08 KB | 1.00 |
Compile_SystemExpression | 103.948 us | 1.9593 us | 2.5477 us | 33.26 | 0.84 | 2 | 0.7324 | 0.4883 | 4.74 KB | 4.40 |
Invoking the compiled delegate compared to the normal delegate and the direct call:
Method | Mean | Error | StdDev | Ratio | Rank | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|
DirectCall | 10.19 ns | 0.108 ns | 0.085 ns | 1.00 | 1 | 0.0051 | 32 B | 1.00 |
CompiledFast_LightExpression | 10.70 ns | 0.089 ns | 0.070 ns | 1.05 | 2 | 0.0051 | 32 B | 1.00 |
CompiledFast_SystemExpression | 10.91 ns | 0.071 ns | 0.066 ns | 1.07 | 2 | 0.0051 | 32 B | 1.00 |
Compiled_SystemExpression | 11.59 ns | 0.098 ns | 0.081 ns | 1.14 | 3 | 0.0051 | 32 B | 1.00 |
FastExpressionCompiler.LightExpression.Expression
is the lightweight version ofSystem.Linq.Expressions.Expression
.It is designed to be adrop-in replacement for the System Expression - just install theFastExpressionCompiler.LightExpression package instead ofFastExpressionCompiler and replace the usings
usingSystem.Linq.Expressions;usingstaticSystem.Linq.Expressions.Expression;
with
usingstaticFastExpressionCompiler.LightExpression.Expression;namespaceFastExpressionCompiler.LightExpression.UnitTests
You may look at it as a bare-bone wrapper for the computation operation node which helps you to compose the computation tree (without messing with the IL emit directly).Itwon't validate operations compatibility for the tree the waySystem.Linq.Expression
does it, and partially why it is so slow.Hopefully you are checking the expression arguments yourself and not waiting for theExpression
exceptions to blow-up.
Creating the expression:
Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|
Create_LightExpression | 156.6 ns | 3.19 ns | 8.18 ns | 151.9 ns | 1.00 | 0.07 | 1 | 0.0827 | 520 B | 1.00 |
Create_SystemExpression | 1,065.0 ns | 14.24 ns | 11.89 ns | 1,069.3 ns | 6.82 | 0.34 | 2 | 0.2060 | 1304 B | 2.51 |
Creating and compiling:
Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
---|---|---|---|---|---|---|---|---|---|---|---|
Create_LightExpression_and_CompileFast | 4.957 us | 0.0986 us | 0.2362 us | 4.913 us | 1.00 | 0.07 | 1 | 0.3510 | 0.3052 | 2.15 KB | 1.00 |
Create_SystemExpression_and_CompileFast | 6.518 us | 0.1889 us | 0.5541 us | 6.300 us | 1.32 | 0.13 | 2 | 0.4578 | 0.4272 | 2.97 KB | 1.38 |
Create_SystemExpression_and_Compile | 205.000 us | 4.0938 us | 7.3819 us | 206.353 us | 41.44 | 2.45 | 3 | 0.9766 | 0.4883 | 7.15 KB | 3.33 |
FastExpressionCompiler
- Provides the
CompileFast
extension methods for theSystem.Linq.Expressions.LambdaExpression
.
FastExpressionCompiler.LightExpression
- Provides the
CompileFast
extension methods forFastExpressionCompiler.LightExpression.LambdaExpression
. - Provides the drop-in expression replacement with the less consumed memory and the faster construction at the cost of the less validation.
- Includes its own
ExpressionVisitor
. - Supports
ToExpression
method to convert backto theSystem.Linq.Expressions.Expression
. - Supports
ToLightExpression
conversion method to convertfrom theSystem.Linq.Expressions.Expression
toFastExpressionCompiler.LightExpression.Expression
.
Both FastExpressionCompiler and FastExpressionCompiler.LightExpression
- Support
ToCSharpString()
method to output the compilable C# code represented by the expression. - Support
ToExpressionString()
method to output the expression construction C# code, so given the expression object you'll get e.g.Expression.Lambda(Expression.New(...))
.
Marten,Rebus,StructureMap,Lamar,ExpressionToCodeLib,NServiceBus,LINQ2DB,MapsterMapper
Considering:Moq,Apex.Serialization
Install from the NuGet and add theusing FastExpressionCompiler;
and replace the call to the.Compile()
with the.CompileFast()
extension method.
Note:CompileFast
has an optional parameterbool ifFastFailedReturnNull = false
to disable fallback toCompile
.
Hoisted lambda expression (created by the C# Compiler):
vara=newA();varb=newB();Expression<Func<X>>expr=()=>newX(a,b);vargetX=expr.CompileFast();varx=getX();
Manually composed lambda expression:
vara=newA();varbParamExpr=Expression.Parameter(typeof(B),"b");varexpr=Expression.Lambda(Expression.New(_ctorX,Expression.Constant(a,typeof(A)),bParamExpr),bParamExpr);varf=expr.CompileFast();varx=f(newB());
Note: You may simplify Expression usage and enable faster refactoring with the C#using static
statement:
usingstaticSystem.Linq.Expressions.Expression;// or// using static FastExpressionCompiler.LightExpression.Expression;vara=newA();varbParamExpr=Parameter(typeof(B),"b");varexpr=Lambda(New(_ctorX,Constant(a,typeof(A)),bParamExpr),bParamExpr);varf=expr.CompileFast();varx=f(newB());
The idea is to provide the fast compilation for the supported expression typesand fallback to the systemExpression.Compile()
for the not supported types:
FEC does not support yet:
Quote
Dynamic
RuntimeVariables
DebugInfo
MemberInit
with theMemberMemberBinding
and theListMemberBinding
binding typesNewArrayInit
multi-dimensional array initializer is not supported yet
To find what nodes are not supported in your expression you may use the technic described below in theDiagnostics section.
The compilation is done by traversing the expression nodes and emitting the IL.The code is tuned for the performance and the minimal memory consumption.
The expression is traversed twice:
- 1st round is to collect the constants and nested lambdas into the closure objects.
- 2nd round is to emit the IL code and create the delegate using the
DynamicMethod
.
If visitor finds the not supported expression node or the error condition,the compilation is aborted, andnull
is returned enabling the fallback to System.Compile()
.
FEC V3 has added powerful diagnostics and code generation tools.
You may pass the optionalCompilerFlags.EnableDelegateDebugInfo
into theCompileFast
methods.
EnableDelegateDebugInfo
adds the diagnostic info into the compiled delegate including its source Expression and compiled IL code.
It can be used as following:
System.Linq.Expressions.Expression<Func<int,Func<int>>>e= n=>()=>n+1;varf=e.CompileFast(flags:CompilerFlags.EnableDelegateDebugInfo);vard=f.TryGetDebugInfo();d.PrintExpression();d.PrintCSharp();d.PrintIL();// available in NET8+
Expand to see the output of the above code...
Output ofd.PrintExpression()
is the valid C#:
varp=newParameterExpression[1];// the parameter expressionsvare=newExpression[3];// the unique expressionsvarexpr=Lambda<Func<int,Func<int>>>(e[0]=Lambda<Func<int>>(e[1]=MakeBinary(ExpressionType.Add,p[0]=Parameter(typeof(int),"n"),e[2]=Constant(1)),newParameterExpression[0]),p[0// (int n)]);
Output ofd.PrintCSharp()
is the valid C#:
var@cs=(Func<int,Func<int>>)((intn)=>//Func<int>(Func<int>)(()=>//intn+1));
Output ofd.PrintIL()
(includes the IL of the nested lambda):
<Caller>0 ldarg.01 ldfld object[] ExpressionCompiler.ArrayClosure.ConstantsAndNestedLambdas6 stloc.07 ldloc.08 ldc.i4.09 ldelem.ref10 stloc.111 ldloc.112 ldc.i4.113 newarr object18 stloc.219 ldloc.220 stfld object[] ExpressionCompiler.NestedLambdaForNonPassedParams.NonPassedParams25 ldloc.226 ldc.i4.027 ldarg.128 box int33 stelem.ref34 ldloc.135 ldfld object ExpressionCompiler.NestedLambdaForNonPassedParams.NestedLambda40 ldloc.241 ldloc.142 ldfld object[] ExpressionCompiler.NestedLambdaForNonPassedParamsWithConstants.ConstantsAndNestedLambdas47 newobj ExpressionCompiler.ArrayClosureWithNonPassedParams(System.Object[], System.Object[])52 call Func<int> ExpressionCompiler.CurryClosureFuncs.Curry(System.Func`2[FastExpressionCompiler.LightExpression.ExpressionCompiler+ArrayClosure,System.Int32], ArrayClosure)57 ret</Caller><0_nested_in_Caller>0 ldarg.01 ldfld object[] ExpressionCompiler.ArrayClosureWithNonPassedParams.NonPassedParams6 ldc.i4.07 ldelem.ref8 unbox.any int13 ldc.i4.114 add15 ret</0_nested_in_Caller>
FEC V3.1 has added the compiler flagCompilerFlags.ThrowOnNotSupportedExpression
.When passed toCompileFast(flags: CompilerFlags.ThrowOnNotSupportedExpression)
and the expression contains not (yet) supported Expression node the compilation will throw the exception instead of returningnull
.
To get the whole list of the not yet supported cases you may check inResult.NotSupported_
enum values.
The Code Generation capabilities are available via theToCSharpString
andToExpressionString
extension methods.
Note: When converting the source expression to either C# code or to the Expression construction code you may findthe// NOT_SUPPORTED_EXPRESSION
comments marking the not supported yet expressions by FEC. So you may test the presence or absence of this comment.
- Using
FastExpressionCompiler.LightExpression.Expression
instead ofSystem.Linq.Expressions.Expression
for the faster expression creation. - Using
.TryCompileWithPreCreatedClosure
and.TryCompileWithoutClosure
methods when you know the expression at hand and may skip the first traversing round, e.g. for the "static" expression which does not contain the bound constants.Note: You cannot skip the 1st round if the expression contains theBlock
,Try
, orGoto
expressions.
Bitten Ice Pop icon icon byIcons8
About
Fast Compiler for C# Expression Trees and the lightweight LightExpression alternative. Diagnostic and code generation tools for the expressions.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Packages0
Uh oh!
There was an error while loading.Please reload this page.