FastExpressionCompiler
Fast Compiler for C# Expression Trees and the lightweight LightExpression alternative. Diagnostic and code generation tools for the expressions.
Install / Use
/learn @dadhi/FastExpressionCompilerREADME
FastExpressionCompiler
<img src="./logo.png" alt="logo"/>Targets .NET 6+, .NET 4.7.2+, .NET Standard 2.0+
NuGet packages:
The project was originally a part of the DryIoc, so check it out ;-)
The problem
ExpressionTree compilation is used by the wide variety of tools, e.g. IoC/DI containers, Serializers, ORMs and OOMs.
But Expression.Compile() is just slow.
Moreover the compiled delegate may be slower than the manually created delegate because of the reasons:
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 also a deep dive to Delegate internals.
The solution
The FastExpressionCompiler .CompileFast() extension method is 10-40x times faster than .Compile().
The compiled delegate may be in 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 the Allocated column in the benchmarks below).
Benchmarks
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 AVX2
DefaultJob : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2
Hoisted expression with the constructor and two arguments in closure
var a = new A();
var b = new B();
Expression<Func<X>> e = () => new X(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 |
Hoisted expression with the static method and two nested lambdas and two arguments in closure
var a = new A();
var b = new B();
Expression<Func<X>> getXExpr = () => CreateX((aa, bb) => new X(aa, bb), new Lazy<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 |
Manually composed expression with parameters and closure
var a = new A();
var bParamExpr = Expression.Parameter(typeof(B), "b");
var expr = 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 vs System.Linq.Expressions.Expression
FastExpressionCompiler.LightExpression.Expression is the lightweight version of System.Linq.Expressions.Expression.
It is designed to be a drop-in replacement for the System Expression - just install the FastExpressionCompiler.LightExpression package instead of FastExpressionCompiler and replace the usings
using System.Linq.Expressions;
using static System.Linq.Expressions.Expression;
wi
