Skip to content
311 changes: 185 additions & 126 deletions src/FastExpressionCompiler/FastExpressionCompiler.cs

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/FastExpressionCompiler/TestTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static class TestTools
public static bool AllowPrintIL = false;
public static bool AllowPrintCS = false;
public static bool AllowPrintExpression = false;
public static bool DisableAssertOpCodes = true; // todo: @wip make it false in the release build

static TestTools()
{
Expand All @@ -43,6 +44,7 @@ public static void AssertOpCodes(this Delegate @delegate, params OpCode[] expect

public static void AssertOpCodes(this MethodInfo method, params OpCode[] expectedCodes)
{
if (DisableAssertOpCodes) return;
var ilReader = ILReaderFactory.Create(method);
if (ilReader is null)
{
Expand Down Expand Up @@ -952,6 +954,8 @@ public sealed class TestRun
public SmallList<TestStats> Stats;
public SmallList<TestFailure> Failures;

// todo: @wip put the output under the feature flag
/// <summary>Will output the failures while running</summary>
public void Run<T>(T test, TestTracking tracking = TestTracking.TrackFailedTestsOnly) where T : ITestX
{
var totalTestCount = TotalTestCount;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(LatestSupportedNet)</TargetFrameworks>
<TargetFrameworks>$(LatestSupportedNet);net8.0</TargetFrameworks>

<OutputType>Exe</OutputType>
<IsTestProject>false</IsTestProject>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.Linq.Expressions;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;

namespace FastExpressionCompiler.Benchmarks;

/*
## Base line with the static method, it seems to be a wrong idea for the improvement, because the closure-bound method is faster as I did discovered a long ago.

BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3775)
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
.NET 8.0 : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2
.NET 9.0 : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2

| Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Rank | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio |
|------------------- |--------- |--------- |----------:|----------:|----------:|------:|--------:|-----:|----------------------:|---------------:|------------------------:|----------:|------------:|
| InvokeCompiled | .NET 8.0 | .NET 8.0 | 0.4365 ns | 0.0246 ns | 0.0192 ns | 1.00 | 0.06 | 1 | 1 | -0 | -0 | - | NA |
| InvokeCompiledFast | .NET 8.0 | .NET 8.0 | 1.0837 ns | 0.0557 ns | 0.0991 ns | 2.49 | 0.25 | 2 | 2 | 0 | 0 | - | NA |
| | | | | | | | | | | | | | |
| InvokeCompiled | .NET 9.0 | .NET 9.0 | 0.5547 ns | 0.0447 ns | 0.0871 ns | 1.02 | 0.22 | 1 | 1 | -0 | -0 | - | NA |
| InvokeCompiledFast | .NET 9.0 | .NET 9.0 | 1.1920 ns | 0.0508 ns | 0.0450 ns | 2.20 | 0.34 | 2 | 2 | 0 | -0 | - | NA |


## Sealing the closure type does not help

| Method | Job | Runtime | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio |
|------------------- |--------- |--------- |----------:|----------:|----------:|----------:|------:|--------:|-----:|----------------------:|------------------------:|---------------:|----------:|------------:|
| InvokeCompiledFast | .NET 8.0 | .NET 8.0 | 1.0066 ns | 0.0209 ns | 0.0233 ns | 0.9973 ns | 1.00 | 0.03 | 2 | 2 | 0 | 0 | - | NA |
| InvokeCompiled | .NET 8.0 | .NET 8.0 | 0.5040 ns | 0.0217 ns | 0.0169 ns | 0.5016 ns | 0.50 | 0.02 | 1 | 1 | -0 | -0 | - | NA |
| | | | | | | | | | | | | | | |
| InvokeCompiledFast | .NET 9.0 | .NET 9.0 | 1.0640 ns | 0.0539 ns | 0.0929 ns | 1.0106 ns | 1.01 | 0.12 | 2 | 2 | 0 | 0 | - | NA |
| InvokeCompiled | .NET 9.0 | .NET 9.0 | 0.5897 ns | 0.0451 ns | 0.0858 ns | 0.6156 ns | 0.56 | 0.09 | 1 | 1 | -0 | -0 | - | NA |

*/
[MemoryDiagnoser, RankColumn]
[HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)]
[DisassemblyDiagnoser(printSource: true, maxDepth: 4)] // for some reason it cannot see inside the method whatever depth I specify
[SimpleJob(RuntimeMoniker.Net90)]
// [SimpleJob(RuntimeMoniker.Net80)]
public class Issue468_InvokeCompiled_vs_InvokeCompiledFast
{
Func<bool> _compiled, _compiledFast;

[GlobalSetup]
public void Setup()
{
var expr = IssueTests.Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET.CreateExpression();
_compiled = expr.CompileSys();
_compiledFast = expr.CompileFast();
}

[Benchmark(Baseline = true)]
public bool InvokeCompiledFast()
{
return _compiledFast();
}

[Benchmark]
public bool InvokeCompiled()
{
return _compiled();
}
}

/*
## Baseline. Does not look good. There is actually a regression I need to find and fix.

| Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
|------------- |--------- |--------- |---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:|
| Compiled | .NET 8.0 | .NET 8.0 | 23.51 us | 0.468 us | 0.715 us | 1.00 | 0.04 | 2 | 0.6714 | 0.6409 | 4.13 KB | 1.00 |
| CompiledFast | .NET 8.0 | .NET 8.0 | 17.63 us | 0.156 us | 0.146 us | 0.75 | 0.02 | 1 | 0.1831 | 0.1526 | 1.16 KB | 0.28 |
| | | | | | | | | | | | | |
| Compiled | .NET 9.0 | .NET 9.0 | 21.27 us | 0.114 us | 0.106 us | 1.00 | 0.01 | 2 | 0.6714 | 0.6409 | 4.13 KB | 1.00 |
| CompiledFast | .NET 9.0 | .NET 9.0 | 16.82 us | 0.199 us | 0.186 us | 0.79 | 0.01 | 1 | 0.1831 | 0.1526 | 1.16 KB | 0.28 |
*/
[MemoryDiagnoser, RankColumn]
[SimpleJob(RuntimeMoniker.Net90)]
[SimpleJob(RuntimeMoniker.Net80)]
public class Issue468_Compile_vs_FastCompile
{
Expression<Func<bool>> _expr;

[GlobalSetup]
public void Setup()
{
_expr = IssueTests.Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET.CreateExpression();
}

[Benchmark(Baseline = true)]
public object CompiledFast()
{
return _expr.CompileFast();
}

[Benchmark]
public object Compiled()
{
return _expr.Compile();
}
}
6 changes: 4 additions & 2 deletions test/FastExpressionCompiler.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ public static void Main()
// BenchmarkRunner.Run<ManuallyComposedLambdaBenchmark.Create>(); // not included in README.md, may be it needs to
// BenchmarkRunner.Run<ManuallyComposedLambdaBenchmark.Create_and_Compile>(); // not included in README.md, may be it needs to

BenchmarkRunner.Run<LightExprVsExpr_Create_ComplexExpr>();
BenchmarkRunner.Run<LightExprVsExpr_CreateAndCompile_ComplexExpr>();
// BenchmarkRunner.Run<LightExprVsExpr_Create_ComplexExpr>();
// BenchmarkRunner.Run<LightExprVsExpr_CreateAndCompile_ComplexExpr>();

//--------------------------------------------

// BenchmarkRunner.Run<Issue468_Compile_vs_FastCompile>();
BenchmarkRunner.Run<Issue468_InvokeCompiled_vs_InvokeCompiledFast>();

// BenchmarkRunner.Run<AccessByRef_vs_ByIGetRefStructImpl>();

Expand Down
24 changes: 12 additions & 12 deletions test/FastExpressionCompiler.IssueTests/EmitHacksTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void DynamicMethod_Emit_Hack()
private static Func<ILGenerator, IList<object>> GetScopeTokens()
{
var dynMethod = new DynamicMethod(string.Empty,
typeof(IList<object>), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(ILGenerator) },
typeof(IList<object>), new[] { typeof(ExpressionCompiler.EmptyClosure), typeof(ILGenerator) },
typeof(ExpressionCompiler), skipVisibility: true);
var il = dynMethod.GetILGenerator();

Expand All @@ -51,7 +51,7 @@ private static Func<ILGenerator, IList<object>> GetScopeTokens()
il.Emit(OpCodes.Ldfld, mTokensField);
il.Emit(OpCodes.Ret);

return (Func<ILGenerator, IList<object>>)dynMethod.CreateDelegate(typeof(Func<ILGenerator, IList<object>>), ExpressionCompiler.EmptyArrayClosure);
return (Func<ILGenerator, IList<object>>)dynMethod.CreateDelegate(typeof(Func<ILGenerator, IList<object>>), ExpressionCompiler.EmptyClosure.Instance);
}
static readonly Func<ILGenerator, IList<object>> getScopeTokens = GetScopeTokens();

Expand All @@ -60,7 +60,7 @@ private static Func<ILGenerator, IList<object>> GetScopeTokens()
private static GetFieldRefDelegate<TFieldHolder, TField> CreateFieldAccessor<TFieldHolder, TField>(FieldInfo field)
{
var dynMethod = new DynamicMethod(string.Empty,
typeof(TField).MakeByRefType(), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(TFieldHolder) },
typeof(TField).MakeByRefType(), new[] { typeof(ExpressionCompiler.EmptyClosure), typeof(TFieldHolder) },
typeof(TFieldHolder), skipVisibility: true);

var il = dynMethod.GetILGenerator();
Expand All @@ -83,7 +83,7 @@ public static Func<int, int> Get_DynamicMethod_Emit_Hack()
var paramCount = 1;

var dynMethod = new DynamicMethod(string.Empty,
typeof(int), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(int) },
typeof(int), new[] { typeof(ExpressionCompiler.EmptyClosure), typeof(int) },
typeof(ExpressionCompiler),
skipVisibility: true);

Expand Down Expand Up @@ -127,7 +127,7 @@ public static Func<int, int> Get_DynamicMethod_Emit_Hack()
mILStream[mLength++] = (byte)OpCodes.Ret.Value;
updateStackSizeDelegate(il, OpCodes.Ret, 0);

return (Func<int, int>)dynMethod.CreateDelegate(typeof(Func<int, int>), ExpressionCompiler.EmptyArrayClosure);
return (Func<int, int>)dynMethod.CreateDelegate(typeof(Func<int, int>), ExpressionCompiler.EmptyClosure.Instance);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand All @@ -153,7 +153,7 @@ public void DynamicMethod_Emit_OpCodes_Call()
public static Func<int, int> Get_DynamicMethod_Emit_OpCodes_Call()
{
var dynMethod = new DynamicMethod(string.Empty,
typeof(int), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(int) },
typeof(int), new[] { typeof(ExpressionCompiler.EmptyClosure), typeof(int) },
typeof(ExpressionCompiler), skipVisibility: true);

var il = dynMethod.GetILGenerator();
Expand All @@ -164,7 +164,7 @@ public static Func<int, int> Get_DynamicMethod_Emit_OpCodes_Call()
// il.Emit(OpCodes.Call, MethodStaticNoArgs);
il.Emit(OpCodes.Ret);

return (Func<int, int>)dynMethod.CreateDelegate(typeof(Func<int, int>), ExpressionCompiler.EmptyArrayClosure);
return (Func<int, int>)dynMethod.CreateDelegate(typeof(Func<int, int>), ExpressionCompiler.EmptyClosure.Instance);
}


Expand All @@ -186,22 +186,22 @@ public void DynamicMethod_Hack_Emit_Newobj()
public static Func<A> Get_DynamicMethod_Emit_Newobj()
{
var dynMethod = new DynamicMethod(string.Empty,
typeof(A), new[] { typeof(ExpressionCompiler.ArrayClosure) },
typeof(A), new[] { typeof(ExpressionCompiler.EmptyClosure) },
typeof(ExpressionCompiler), skipVisibility: true);

var il = dynMethod.GetILGenerator();

il.Emit(OpCodes.Newobj, _ctor);
il.Emit(OpCodes.Ret);

return (Func<A>)dynMethod.CreateDelegate(typeof(Func<A>), ExpressionCompiler.EmptyArrayClosure);
return (Func<A>)dynMethod.CreateDelegate(typeof(Func<A>), ExpressionCompiler.EmptyClosure.Instance);
}

public static Func<A> Get_DynamicMethod_Hack_Emit_Newobj()
{
var dynMethod = new DynamicMethod(string.Empty,
typeof(A), new[] { typeof(ExpressionCompiler.ArrayClosure) },
typeof(ExpressionCompiler), skipVisibility: true);
typeof(A), new[] { typeof(ExpressionCompiler.EmptyClosure) },
typeof(ExpressionCompiler.EmptyClosure), true);

var il = dynMethod.GetILGenerator();
var ilType = il.GetType();
Expand All @@ -228,7 +228,7 @@ public static Func<A> Get_DynamicMethod_Hack_Emit_Newobj()

il.Emit(OpCodes.Ret);

return (Func<A>)dynMethod.CreateDelegate(typeof(Func<A>), ExpressionCompiler.EmptyArrayClosure);
return (Func<A>)dynMethod.CreateDelegate(typeof(Func<A>), ExpressionCompiler.EmptyClosure.Instance);
}

public class A
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ public struct Issue461_InvalidProgramException_when_null_checking_type_by_ref :
{
public int Run()
{
Case_equal_nullable_and_object_null_without_in_paramater();
Case_equal_nullable_and_object_null();
Case_equal_nullable_and_nullable_null_on_the_left();
Case_not_equal_nullable_decimal();
Original_case();
Original_case_null_on_the_right();
return 5;
return 6;
}

private class Target
Expand Down Expand Up @@ -123,6 +124,27 @@ public void Case_equal_nullable_and_object_null()
);
}

public void Case_equal_nullable_and_object_null_without_in_paramater()
{
var p = Parameter(typeof(XX?), "xx");

var expr = Lambda<Func<XX?, bool>>(
MakeBinary(ExpressionType.Equal, p, Constant(null)),
p);

expr.PrintCSharp();

var fs = expr.CompileSys();
fs.PrintIL();
Asserts.IsTrue(fs(null));
Asserts.IsFalse(fs(new XX()));

var ff = expr.CompileFast(false);
ff.PrintIL();
Asserts.IsTrue(ff(null));
Asserts.IsFalse(ff(new XX()));
}

public void Case_equal_nullable_and_nullable_null_on_the_left()
{
var p = Parameter(typeof(XX?).MakeByRefType(), "xx");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;

#if LIGHT_EXPRESSION
using ExpressionType = System.Linq.Expressions.ExpressionType;
using static FastExpressionCompiler.LightExpression.Expression;
namespace FastExpressionCompiler.LightExpression.IssueTests;
#else
using System.Linq.Expressions;
using static System.Linq.Expressions.Expression;
namespace FastExpressionCompiler.IssueTests;
#endif

public struct Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET : ITestX
{
public void Run(TestRun t)
{
Original_expression(t);
Original_expression_with_closure(t);
}

// exposing for benchmarking
public static Expression<Func<bool>> CreateExpression(
#if LIGHT_EXPRESSION
bool addClosure = false
#endif
)
{
var e = new Expression[11]; // the unique expressions
var expr = Lambda<Func<bool>>(
e[0] = MakeBinary(ExpressionType.Equal,

e[1] = MakeBinary(ExpressionType.Equal,
e[2] = MakeBinary(ExpressionType.Add,
e[3] = Constant(1),
e[4] = Constant(2)),
e[5] = MakeBinary(ExpressionType.Add,
e[6] = Constant(5),
e[7] = Constant(-2))),

e[8] = MakeBinary(ExpressionType.Equal,
e[9] = Constant(42),
#if LIGHT_EXPRESSION
e[10] = !addClosure ? Constant(42) : ConstantRef(42)
#else
e[10] = Constant(42)
#endif
)), new ParameterExpression[0]);
return expr;
}

public void Original_expression(TestContext t)
{
var expr = CreateExpression();

expr.PrintCSharp();
// var @cs = (Func<bool>)(() => //bool
// (1 + 2 == 5 + -2) == 42 == 42);

var fs = expr.CompileSys();
fs.PrintIL();
t.IsTrue(fs());

var ff = expr.CompileFast(false);
ff.PrintIL();
t.IsTrue(ff());

// ff.AssertOpCodes(
// OpCodes.Ldarg_1,
// OpCodes.Ldind_Ref,
// OpCodes.Ldnull,
// OpCodes.Ceq,
// OpCodes.Ret
// );
}
public void Original_expression_with_closure(TestContext t)
{
#if LIGHT_EXPRESSION
var expr = CreateExpression(true);

expr.PrintCSharp();

var fs = expr.CompileSys();
fs.PrintIL();
t.IsTrue(fs());

var ff = expr.CompileFast(false);
ff.PrintIL();
t.IsTrue(ff());
#endif
}
}
Loading
Loading