/FastExpressionCompiler

Fast ExpressionTree compiler to delegate

Primary LanguageC#MIT LicenseMIT

FastExpressionCompiler

FastExpressionCompiler NuGet Badgefuget.org package api diff
FastExpressionCompiler.LightExpression NuGet Badgefuget.org package last version

license

  • Windows: Windows build
  • Linux, MacOS: Linux build

Targets: .NET 4.5+, .NET Standard 1.3, .NET Standard 2.0
Originally was developed as a part of DryIoc, so check it out ;-)

The problem

ExpressionTree compilation is used by wide range of tools, e.g. IoC/DI containers, Serializers, OO Mappers. But Expression.Compile() is just slow. Moreover, the compiled delegate may be slower than 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.

.CompileFast() is 10-30x times faster than .Compile().
The compiled delegate may be in some cases 15x times faster than the one produced by .Compile().

Note: The actual performance may vary depending on multiple factors: platform, how complex is expression, does it have a closure over the values, does it contain nested lambdas, etc.

How to install

Install from NuGet or grab a single FastExpressionCompiler.cs file.

Some users

Marten, Rebus, StructureMap, Lamar, ExpressionToCodeLib, NServiceBus.

Considering: Moq, LINQ to DB

How to use

Add reference to FastExpressionCompiler and replace call to .Compile() with .CompileFast() extension method.

Note: CompileFast has an optional parameter bool ifFastFailedReturnNull = false to disable fallback to Compile.

Examples

Hoisted lambda expression (created by compiler):

var a = new A(); var b = new B();
Expression<Func<X>> expr = () => new X(a, b);

var getX = expr.CompileFast();
var x = getX();

Manually composed lambda expression:

var a = new A();
var bParamExpr = Expression.Parameter(typeof(B), "b");
var expr = Expression.Lambda(
    Expression.New(typeof(X).GetTypeInfo().DeclaredConstructors.First(),
        Expression.Constant(a, typeof(A)), bParamExpr),
    bParamExpr);

var getX = expr.CompileFast();
var x = getX(new B());

Note: Simplify your life in C# 6+ with using static

using static System.Linq.Expressions.Expression;
// or
// using static FastExpressionCompiler.LightExpression.Expression;

var a = new A();
var bParamExpr = Parameter(typeof(B), "b");
var expr = Lambda(
    New(typeof(X).GetTypeInfo().DeclaredConstructors.First(), Constant(a, typeof(A)), bParamExpr),
    bParamExpr);

var x = expr.CompileFast()(new B());

Benchmarks

BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.523 (1803/April2018Update/Redstone4)
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
Frequency=2156255 Hz, Resolution=463.7670 ns, Timer=TSC
.NET Core SDK=2.2.100
  [Host]     : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT
  DefaultJob : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT

Hoisted expression with 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 Gen 0/1k Op Gen 1/1k Op Gen 2/1k Op Allocated Memory/Op
CompileFast 7.996 us 0.0638 us 0.0565 us 1.00 0.00 0.4883 0.2441 0.0305 2.26 KB
Compile 242.974 us 1.4929 us 1.3964 us 30.39 0.26 0.7324 0.2441 - 4.45 KB

Invoking compiled delegate (also comparing to the direct constructor call):

Method Mean Error StdDev Ratio RatioSD Gen 0/1k Op Gen 1/1k Op Gen 2/1k Op Allocated Memory/Op
DirectConstructorCall 6.203 ns 0.1898 ns 0.3470 ns 0.76 0.06 0.0068 - - 32 B
FastCompiledLambda 7.840 ns 0.2010 ns 0.1881 ns 1.00 0.00 0.0068 - - 32 B
CompiledLambda 12.313 ns 0.1124 ns 0.1052 ns 1.57 0.04 0.0068 - - 32 B

Hoisted expression with 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 Gen 0 Gen 1 Gen 2 Allocated
Expression_Compile 481.33 us 0.6025 us 0.5031 us 29.47 0.09 2.4414 0.9766 - 11.95 KB
Expression_CompileFast 16.33 us 0.0555 us 0.0492 us 1.00 0.00 1.0986 0.5493 0.0916 5.13 KB

Invoking compiled delegate comparing to direct method call:

Method Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
DirectMethodCall 50.78 ns 0.1651 ns 0.1544 ns 0.92 0.01 0.0356 - - 168 B
Invoke_Compiled 1,385.24 ns 2.8196 ns 2.6375 ns 25.10 0.33 0.0553 - - 264 B
Invoke_CompiledFast 55.20 ns 0.8883 ns 0.7875 ns 1.00 0.00 0.0220 - - 104 B

Manually composed expression with parameters and closure

var a = new A();
var bParamExpr = Expression.Parameter(typeof(B), "b");
var expr = Expression.Lambda(
    Expression.New(typeof(X).GetTypeInfo().DeclaredConstructors.First(),
        Expression.Constant(a, typeof(A)), bParamExpr),
    bParamExpr);

Compiling expression:

Method Mean Error StdDev Ratio RatioSD Gen 0/1k Op Gen 1/1k Op Gen 2/1k Op Allocated Memory/Op
CompileFastWithPreCreatedClosureLightExpression 4.892 us 0.0965 us 0.0948 us 1.00 0.00 0.3281 0.1602 0.0305 1.5 KB
CompileFastWithPreCreatedClosure 5.186 us 0.0896 us 0.0795 us 1.06 0.02 0.3281 0.1602 0.0305 1.5 KB
CompileFast 7.257 us 0.0648 us 0.0606 us 1.49 0.03 0.4349 0.2136 0.0305 1.99 KB
Compile 176.107 us 1.3451 us 1.2582 us 36.05 0.75 0.9766 0.4883 - 4.7 KB

Invoking compiled delegate compared to the normal delegate:

Method Mean Error StdDev Ratio Gen 0/1k Op Gen 1/1k Op Gen 2/1k Op Allocated Memory/Op
FastCompiledLambdaWithPreCreatedClosureLE 10.64 ns 0.0404 ns 0.0358 ns 1.00 0.0068 - - 32 B
DirectLambdaCall 10.65 ns 0.0601 ns 0.0533 ns 1.00 0.0068 - - 32 B
FastCompiledLambda 10.98 ns 0.0434 ns 0.0406 ns 1.03 0.0068 - - 32 B
FastCompiledLambdaWithPreCreatedClosure 11.10 ns 0.0369 ns 0.0345 ns 1.04 0.0068 - - 32 B
CompiledLambda 11.13 ns 0.0620 ns 0.0518 ns 1.05 0.0068 - - 32 B

FEC.LightExpression.Expression vs Expression

FastExpressionCompiler.LightExpression.Expression is the lightweight version of System.Linq.Expressions.Expression. It is designed to be a drop-in replacement for System Expression - just install FastExpressionCompiler.LightExpression package instead of FastExpressionCompiler and replace the usings

using System.Linq.Expressions;
using static System.Linq.Expressions.Expression;

with

using static FastExpressionCompiler.LightExpression.Expression;
namespace FastExpressionCompiler.LightExpression.UnitTests

You may look at it as a bare wrapper over expression node which helps you to compose the computation tree.
It won't do any node compatibility verification for the tree as the Expression does and why the creation of the latter is slower. Hopefully you are checking the expression arguments yourself and not waiting for Expression exceptions to blow up - then you are safe.

Sample expression

Creating expression:

Method Mean Error StdDev Ratio RatioSD Gen 0/1k Op Gen 1/1k Op Gen 2/1k Op Allocated Memory/Op
CreateLightExpression 389.5 ns 0.9547 ns 0.7972 ns 1.00 0.00 0.1693 - - 800 B
CreateExpression 3,574.7 ns 8.0032 ns 7.4862 ns 9.18 0.02 0.2823 - - 1344 B

Creating and compiling:

Method Mean Error StdDev Ratio RatioSD Gen 0/1k Op Gen 1/1k Op Gen 2/1k Op Allocated Memory/Op
CreateLightExpression_and_CompileFast 12.68 us 0.0555 us 0.0492 us 1.00 0.00 1.4343 0.7172 0.0458 6.61 KB
CreateExpression_and_CompileFast 19.26 us 0.2559 us 0.2268 us 1.52 0.02 1.5564 0.7629 0.0305 7.23 KB
CreateExpression_and_Compile 260.67 us 1.7431 us 1.6305 us 20.54 0.14 1.4648 0.4883 - 7.16 KB

How it works

The idea is to provide fast compilation for supported expression types, and fallback to system Expression.Compile() for not yet supported types.

Note: As of v1.9 most of the types are supported, please open issue if something is not ;-)

Compilation is done by visiting expression nodes and emitting the IL. The code is tuned for performance and minimal memory consumption.

Expression is visited in two rounds (you can skip the first one with up-front knowledge):

  1. To collect constants and nested lambdas into closure objects
  2. To emit the IL and create the delegate from a DynamicMethod

If visitor finds a not supported expression node, the compilation is aborted, and null is returned enabling the fallback to normal .Compile().

Additional optimizations

  1. Using FastExpressionCompiler.LightExpression.Expression instead of System.Linq.Expressions.Expression for the lightweight expression creation.
    It won't speed-up compilation alone but may speed-up the construction.
  2. Using expr.TryCompileWithPreCreatedClosure and expr.TryCompileWithoutClosure when you know the expression at hand and may optimize for delegate with the closure or for "static" delegate.

Both optimizations are visible in benchmark results: search for LightExpression and FastCompiledLambdaWithPreCreatedClosure respectively.