diff --git a/src/FastExpressionCompiler.LightExpression/Expression.cs b/src/FastExpressionCompiler.LightExpression/Expression.cs index 6fffc900..8a026693 100644 --- a/src/FastExpressionCompiler.LightExpression/Expression.cs +++ b/src/FastExpressionCompiler.LightExpression/Expression.cs @@ -221,9 +221,6 @@ public static ConstantExpression ConstantNull(Type type = null) => public static ConstantExpression ConstantOf(T value) => value == null ? ConstantNull() : new ValueConstantExpression(value); - [MethodImpl((MethodImplOptions)256)] - public static int TryGetIntConstantValue(Expression e) => ((IntConstantExpression)e).IntValue; - [RequiresUnreferencedCode(Trimming.Message)] public static NewExpression New(Type type) { @@ -3914,9 +3911,8 @@ public sealed class TypedValueConstantExpression : ConstantExpression public sealed class IntConstantExpression : ConstantExpression { public override Type Type => typeof(int); - public override object Value => IntValue; - public readonly int IntValue; - internal IntConstantExpression(int value) => IntValue = value; + public override object Value { get; } + internal IntConstantExpression(int value) => Value = value; } public class NewExpression : Expression, IArgumentProvider diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 70f7f94a..455a723e 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -76,7 +76,10 @@ public enum CompilerFlags : byte /// Adds the Expression, ExpressionString, and CSharpString to the delegate closure for the debugging inspection EnableDelegateDebugInfo = 1 << 1, /// When the flag is set then instead of the returning `null` the specific exception is thrown*346 - ThrowOnNotSupportedExpression = 1 << 2 + ThrowOnNotSupportedExpression = 1 << 2, + /// Will try to Interpret arithmetic, logical, comparison expressions for the primitive types, + /// and emit the IL the result only instead of the whole computation. + DisableInterpreter = 1 << 4 } /// FEC Not Supported exception @@ -237,7 +240,7 @@ public static Func CompileFast(this ExpressionCompiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. @@ -490,6 +493,13 @@ internal static object TryCompileBoundToFirstClosureParam(Type delegateType, Exp Type[] closurePlusParamTypes, Type returnType, CompilerFlags flags) { #endif + // Try to avoid compilation altogether for Func delegates via Interpreter, see #468 + if ((flags & CompilerFlags.DisableInterpreter) == 0 & + returnType == typeof(bool) & closurePlusParamTypes.Length == 1 + && Interpreter.IsCandidateForInterpretation(bodyExpr) + && Interpreter.TryInterpretBoolean(out var result, bodyExpr)) + return result ? Interpreter.TrueFunc : Interpreter.FalseFunc; + // The method collects the info from the all nested lambdas deep down up-front and de-duplicates the lambdas as well. var closureInfo = new ClosureInfo(ClosureStatus.ToBeCollected); if (!TryCollectBoundConstants(ref closureInfo, bodyExpr, paramExprs, null, ref closureInfo.NestedLambdas, flags)) @@ -511,6 +521,9 @@ internal static object TryCompileBoundToFirstClosureParam(Type delegateType, Exp closure = new DebugArrayClosure(constantsAndNestedLambdas, debugExpr); } + // note: @slow this is what System.Compiles does and which makes the compilation 10x slower, but the invocation become faster by a single branch instruction + // var method = new DynamicMethod(string.Empty, returnType, closurePlusParamTypes, true); + // this is FEC way, significantly faster compilation, but +1 branch instruction in the invocation var method = new DynamicMethod(string.Empty, returnType, closurePlusParamTypes, typeof(ArrayClosure), true); // todo: @perf can we just count the Expressions in the TryCollect phase and use it as N * 4 or something? @@ -533,6 +546,7 @@ internal static object TryCompileBoundToFirstClosureParam(Type delegateType, Exp private static readonly Type[] _closureAsASingleParamType = { typeof(ArrayClosure) }; private static readonly Type[][] _closureTypePlusParamTypesPool = new Type[8][]; // todo: @perf @mem could we use this for other Type arrays? + // todo: @perf optimize #if LIGHT_EXPRESSION private static Type[] RentOrNewClosureTypeToParamTypes(IParameterProvider paramExprs) { @@ -1192,7 +1206,7 @@ public static Result TryCollectInfo(ref ClosureInfo closure, Expression expr, return Result.OK; } - if (expr == NullConstant || expr == FalseConstant || expr == TrueConstant || expr is IntConstantExpression n) + if (expr == NullConstant | expr == FalseConstant | expr == TrueConstant || expr is IntConstantExpression) return r; #endif var constantExpr = (ConstantExpression)expr; @@ -2073,6 +2087,13 @@ public static bool TryEmit(Expression expr, case ExpressionType.LessThanOrEqual: case ExpressionType.Equal: case ExpressionType.NotEqual: + if ((setup & CompilerFlags.DisableInterpreter) == 0 && expr.Type.IsPrimitive && + Interpreter.TryInterpretBoolean(out var boolResult, expr)) + { + if ((parent & ParentFlags.IgnoreResult) == 0) + il.Demit(boolResult ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + return true; + } var binaryExpr = (BinaryExpression)expr; return TryEmitComparison(binaryExpr.Left, binaryExpr.Right, expr.Type, nodeType, paramExprs, il, ref closure, setup, parent); @@ -2090,10 +2111,12 @@ public static bool TryEmit(Expression expr, case ExpressionType.ExclusiveOr: case ExpressionType.LeftShift: case ExpressionType.RightShift: + // todo: @wip interpreter return TryEmitArithmetic(((BinaryExpression)expr).Left, ((BinaryExpression)expr).Right, nodeType, expr.Type, paramExprs, il, ref closure, setup, parent); case ExpressionType.AndAlso: case ExpressionType.OrElse: + // todo: @wip interpreter return TryEmitLogicalOperator((BinaryExpression)expr, nodeType, paramExprs, il, ref closure, setup, parent); case ExpressionType.Coalesce: @@ -2963,7 +2986,8 @@ public static bool TryEmitNonByRefNonValueTypeParameter(ParameterExpression para --paramIndex; if (paramIndex != -1) { - ++paramIndex; // shift parameter index by one, because the first one will be closure + if ((closure.Status & ClosureStatus.ShouldBeStaticMethod) == 0) + ++paramIndex; // shift parameter index by one, because the first one will be closure if (closure.LastEmitIsAddress) EmitLoadArgAddress(il, paramIndex); else @@ -3422,19 +3446,19 @@ public static bool TryEmitConstant(ConstantExpression expr, Type exprType, ILGen il.Demit(OpCodes.Ldnull); ok = true; } - else if (expr == FalseConstant) + else if (expr == FalseConstant | expr == ZeroConstant) { il.Demit(OpCodes.Ldc_I4_0); ok = true; } - else if (expr == TrueConstant) + else if (expr == TrueConstant | expr == OneConstant) { il.Demit(OpCodes.Ldc_I4_1); ok = true; } else if (expr is IntConstantExpression n) { - EmitLoadConstantInt(il, n.IntValue); + EmitLoadConstantInt(il, (int)n.Value); ok = true; } #endif @@ -4570,9 +4594,8 @@ private static bool TryEmitAssignToParameterOrVariable( while (paramIndex != -1 && !ReferenceEquals(paramExprs.GetParameter(paramIndex), left)) --paramIndex; if (paramIndex != -1) { - // shift parameter index by one, because the first one will be closure if ((closure.Status & ClosureStatus.ShouldBeStaticMethod) == 0) - ++paramIndex; + ++paramIndex; // shift parameter index by one, because the first one will be closure var isLeftByRef = left.IsByRef; if (isLeftByRef) @@ -6045,12 +6068,13 @@ private static Expression TryReduceCondition(Expression testExpr) { // simplify the not `==` -> `!=`, `!=` -> `==` var op = TryReduceCondition(((UnaryExpression)testExpr).Operand); - if (op.NodeType == ExpressionType.Equal) // ensures that it is a BinaryExpression + var nodeType = op.NodeType; + if (nodeType == ExpressionType.Equal) // ensures that it is a BinaryExpression { var binOp = (BinaryExpression)op; return NotEqual(binOp.Left, binOp.Right); } - else if (op.NodeType == ExpressionType.NotEqual) // ensures that it is a BinaryExpression + else if (nodeType == ExpressionType.NotEqual) // ensures that it is a BinaryExpression { var binOp = (BinaryExpression)op; return Equal(binOp.Left, binOp.Right); @@ -6058,7 +6082,8 @@ private static Expression TryReduceCondition(Expression testExpr) } else if (testExpr is BinaryExpression b) { - if (b.NodeType == ExpressionType.OrElse || b.NodeType == ExpressionType.Or) + var nodeType = b.NodeType; + if (nodeType == ExpressionType.OrElse | nodeType == ExpressionType.Or) { if (b.Left is ConstantExpression lc && lc.Value is bool lcb) return lcb ? lc : TryReduceCondition(b.Right); @@ -6066,7 +6091,7 @@ private static Expression TryReduceCondition(Expression testExpr) if (b.Right is ConstantExpression rc && rc.Value is bool rcb && !rcb) return TryReduceCondition(b.Left); } - else if (b.NodeType == ExpressionType.AndAlso || b.NodeType == ExpressionType.And) + else if (nodeType == ExpressionType.AndAlso | nodeType == ExpressionType.And) { if (b.Left is ConstantExpression lc && lc.Value is bool lcb) return !lcb ? lc : TryReduceCondition(b.Right); @@ -6365,10 +6390,414 @@ private static void EmitLoadArgAddress(ILGenerator il, int paramIndex) il.Demit(OpCodes.Ldarga, (short)paramIndex); } } + + /// Interpreter + public static class Interpreter + { + /// Always returns true + public static readonly Func TrueFunc = static () => true; + /// Always returns false + public static readonly Func FalseFunc = static () => false; + + /// Single instance of true object + public static readonly object TrueObject = true; + /// Single instance of false object + public static readonly object FalseObject = false; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static T UnreachableCase() + { + throw new InvalidCastException("Unreachable switch case reached"); + } + + /// Operation accepting bool inputs and producing bool output + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLogical(ExpressionType nodeType) => + nodeType == ExpressionType.AndAlso | + nodeType == ExpressionType.OrElse | + nodeType == ExpressionType.Not; + + /// Operation accepting IComparable inputs and producing bool output + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsComparison(ExpressionType nodeType) => + nodeType == ExpressionType.Equal | + nodeType == ExpressionType.NotEqual | + nodeType == ExpressionType.GreaterThan | + nodeType == ExpressionType.GreaterThanOrEqual | + nodeType == ExpressionType.LessThan | + nodeType == ExpressionType.LessThanOrEqual; + + /// Operation accepting the same primitive type inputs (or of the coalescing types) and producing the "same" primitive type output + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsArithmetic(ExpressionType nodeType) => + nodeType == ExpressionType.Add | + nodeType == ExpressionType.Subtract | + nodeType == ExpressionType.Multiply | + nodeType == ExpressionType.Divide | + nodeType == ExpressionType.Modulo | + nodeType == ExpressionType.Negate; + + /// Eval negate + public static object DoNegateOrNull(object operand) + { + return Type.GetTypeCode(operand.GetType()) switch + { + TypeCode.SByte => -(sbyte)operand, + TypeCode.Byte => -(byte)operand, + TypeCode.Int16 => -(short)operand, + TypeCode.UInt16 => -(ushort)operand, + TypeCode.Int32 => -(int)operand, + TypeCode.UInt32 => -(uint)operand, + TypeCode.Int64 => -(long)operand, + TypeCode.Single => -(float)operand, + TypeCode.Double => -(double)operand, + TypeCode.Decimal => -(decimal)operand, + TypeCode.UInt64 => null, + _ => null, + }; + } + + /// Interpret arithmetic. The types of the left and the right operands assumed to be the same. + /// The Expression.Add, Divide, etc, expects the operands to be of the same type + public static object DoArithmeticOrNull(object left, object right, ExpressionType nodeType) + { + Debug.Assert(left != null && right != null, "left and right should not be null"); + Debug.Assert(left.GetType() == right.GetType(), "left and right should be of the same type"); + + var leftCode = Type.GetTypeCode(left.GetType()); + return nodeType switch + { + ExpressionType.Add => leftCode switch + { + // Systems expression does not define the Add for sbyte and byte, but let's keep it here because it is allowed in C# and LightExpression + TypeCode.SByte => (sbyte)left + (sbyte)right, + TypeCode.Byte => (byte)left + (byte)right, + // the rest + TypeCode.Int16 => (short)left + (short)right, + TypeCode.UInt16 => (ushort)left + (ushort)right, + TypeCode.Int32 => (int)left + (int)right, + TypeCode.UInt32 => (uint)left + (uint)right, + TypeCode.Int64 => (long)left + (long)right, + TypeCode.UInt64 => (ulong)left + (ulong)right, + TypeCode.Single => (float)left + (float)right, + TypeCode.Double => (double)left + (double)right, + TypeCode.Decimal => (decimal)left + (decimal)right, + _ => null, + }, + ExpressionType.Subtract => leftCode switch + { + TypeCode.SByte => (sbyte)left - (sbyte)right, + TypeCode.Byte => (byte)left - (byte)right, + TypeCode.Int16 => (short)left - (short)right, + TypeCode.UInt16 => (ushort)left - (ushort)right, + TypeCode.Int32 => (int)left - (int)right, + TypeCode.UInt32 => (uint)left - (uint)right, + TypeCode.Int64 => (long)left - (long)right, + TypeCode.UInt64 => (ulong)left - (ulong)right, + TypeCode.Single => (float)left - (float)right, + TypeCode.Double => (double)left - (double)right, + TypeCode.Decimal => (decimal)left - (decimal)right, + _ => null, + }, + ExpressionType.Multiply => leftCode switch + { + TypeCode.SByte => (sbyte)left * (sbyte)right, + TypeCode.Byte => (byte)left * (byte)right, + TypeCode.Int16 => (short)left * (short)right, + TypeCode.UInt16 => (ushort)left * (ushort)right, + TypeCode.Int32 => (int)left * (int)right, + TypeCode.UInt32 => (uint)left * (uint)right, + TypeCode.Int64 => (long)left * (long)right, + TypeCode.UInt64 => (ulong)left * (ulong)right, + TypeCode.Single => (float)left * (float)right, + TypeCode.Double => (double)left * (double)right, + TypeCode.Decimal => (decimal)left * (decimal)right, + _ => null, + }, + ExpressionType.Divide => leftCode switch + { + TypeCode.SByte => (sbyte)left / (sbyte)right, + TypeCode.Byte => (byte)left / (byte)right, + TypeCode.Int16 => (short)left / (short)right, + TypeCode.UInt16 => (ushort)left / (ushort)right, + TypeCode.Int32 => (int)left / (int)right, + TypeCode.UInt32 => (uint)left / (uint)right, + TypeCode.Int64 => (long)left / (long)right, + TypeCode.UInt64 => (ulong)left / (ulong)right, + TypeCode.Single => (float)left / (float)right, + TypeCode.Double => (double)left / (double)right, + TypeCode.Decimal => (decimal)left / (decimal)right, + _ => null, + }, + ExpressionType.Modulo => leftCode switch + { + TypeCode.SByte => (sbyte)left % (sbyte)right, + TypeCode.Byte => (byte)left % (byte)right, + TypeCode.Int16 => (short)left % (short)right, + TypeCode.UInt16 => (ushort)left % (ushort)right, + TypeCode.Int32 => (int)left % (int)right, + TypeCode.UInt32 => (uint)left % (uint)right, + TypeCode.Int64 => (long)left % (long)right, + TypeCode.UInt64 => (ulong)left % (ulong)right, + TypeCode.Single => (float)left % (float)right, + TypeCode.Double => (double)left % (double)right, + TypeCode.Decimal => (decimal)left % (decimal)right, + _ => null, + }, + _ => null, + }; + } + + public static class ZeroDefault + { + public static readonly object Instance = default(T); + } + + public static object GetZeroDefaultObject(TypeCode typeCode) + { + return typeCode switch + { + TypeCode.Boolean => FalseObject, + TypeCode.SByte => ZeroDefault.Instance, + TypeCode.Byte => ZeroDefault.Instance, + TypeCode.Int16 => ZeroDefault.Instance, + TypeCode.UInt16 => ZeroDefault.Instance, + TypeCode.Int32 => ZeroDefault.Instance, + TypeCode.UInt32 => ZeroDefault.Instance, + TypeCode.Int64 => ZeroDefault.Instance, + TypeCode.UInt64 => ZeroDefault.Instance, + TypeCode.Single => ZeroDefault.Instance, + TypeCode.Double => ZeroDefault.Instance, + TypeCode.Decimal => ZeroDefault.Instance, + _ => null, + }; + } + + /// Fast, mostly negative check to skip or proceed with interpretation. + /// Depending on the context you may avoid calling it because you know the interpreted expression beforehand, + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsCandidateForInterpretation(Expression expr) + { + var nodeType = expr.NodeType; + return + nodeType == ExpressionType.Constant | + nodeType == ExpressionType.Default | + nodeType == ExpressionType.Convert | + nodeType == ExpressionType.Not | + nodeType == ExpressionType.Negate | + expr is BinaryExpression; + } + + /// In case of exception FEC will emit the whole computation to throw exception in the invocation phase + public static bool TryInterpretBoolean(out bool result, Expression expr) + { + try + { + if (TryInterpretPrimitive(out var resultObj, expr)) + { + result = resultObj == TrueObject; + return true; + } + } + catch + { + // eat up the expression this time + } + result = false; + return false; + } + + /// Tries to interpret the expression of the Primitive type of Constant, Convert, Logical, Comparison, Arithmetic. + public static bool TryInterpretPrimitive(out object result, Expression expr) + { + Debug.Assert(expr.Type.IsPrimitive); + result = null; + + // The order of the checks for the type of the expression is deliberate, + // because we are starting with complex expressions first. + // And for the simplest ones like a Constant? the method may not be even called. + // Instead, the Constant check and interpretation may be done inline. + + var nodeType = expr.NodeType; + if (nodeType == ExpressionType.Not) + { + var unaryExpr = (UnaryExpression)expr; + var operandExpr = unaryExpr.Operand; + if (operandExpr is ConstantExpression co) + { +#if LIGHT_EXPRESSION + if (co.RefField != null) return false; +#endif + result = (bool)co.Value ? FalseObject : TrueObject; + return true; + } + if (!TryInterpretPrimitive(out var boolVal, operandExpr)) + return false; + result = boolVal == TrueObject ? FalseObject : TrueObject; + return true; + } + + if (IsLogical(nodeType)) + { + var binaryExpr = (BinaryExpression)expr; + + // Interpreting the left part as the first candidate for the result + var left = binaryExpr.Left; + if (left is ConstantExpression lc) + { +#if LIGHT_EXPRESSION + if (lc.RefField != null) return false; +#endif + result = (bool)lc.Value ? TrueObject : FalseObject; + } + else if (!TryInterpretPrimitive(out result, left)) + return false; + + // Short circuit the interpretation, because this is an actual logic of these logical operations + if (result == TrueObject & nodeType == ExpressionType.OrElse || + result == FalseObject & nodeType == ExpressionType.AndAlso) + return true; + + // If the first part is not enough to decide of the expression result, go right + var right = binaryExpr.Right; + if (right is ConstantExpression rc) + { +#if LIGHT_EXPRESSION + if (rc.RefField != null) return false; +#endif + result = (bool)rc.Value ? TrueObject : FalseObject; + return true; + } + return TryInterpretPrimitive(out result, right); + } + + var isComparison = IsComparison(nodeType); + if (isComparison || IsArithmetic(nodeType)) + { + var binaryExpr = (BinaryExpression)expr; + + // Interpreting left part + var left = binaryExpr.Left; + object leftVal = null; + if (left is ConstantExpression lc) + { +#if LIGHT_EXPRESSION + if (lc.RefField != null) return false; +#endif + leftVal = lc.Value; + } + else if (!TryInterpretPrimitive(out leftVal, left)) + return false; + + // Interpreting right part + var right = binaryExpr.Right; + object rightVal = null; + if (right is ConstantExpression rc) + { +#if LIGHT_EXPRESSION + if (rc.RefField != null) return false; +#endif + rightVal = rc.Value; + } + else if (!TryInterpretPrimitive(out rightVal, right)) + return false; + + // Now do the operation on the left and right + if (isComparison) + { + if (nodeType == ExpressionType.Equal | nodeType == ExpressionType.NotEqual) + { + var boolVal = leftVal.Equals(rightVal); + result = nodeType == ExpressionType.Equal + ? (boolVal ? TrueObject : FalseObject) + : (boolVal ? FalseObject : TrueObject); + } + else + { + // Assuming that the both sides are of the same type, we can use only the left one for comparison + var cmp = leftVal as IComparable; + if (cmp == null) + return false; + var res = cmp.CompareTo(rightVal); + var boolVal = nodeType switch + { + ExpressionType.GreaterThan => res > 0, + ExpressionType.GreaterThanOrEqual => res >= 0, + ExpressionType.LessThan => res < 0, + ExpressionType.LessThanOrEqual => res <= 0, + _ => UnreachableCase(), + }; + result = boolVal ? TrueObject : FalseObject; + } + return true; + } + + // For Arithmetic + result = DoArithmeticOrNull(leftVal, rightVal, nodeType); + return result != null; + } + + if (nodeType == ExpressionType.Negate) + { + var unaryExpr = (UnaryExpression)expr; + var operandExpr = unaryExpr.Operand; + object val = null; + if (operandExpr is ConstantExpression co) + { +#if LIGHT_EXPRESSION + if (co.RefField != null) return false; +#endif + val = co.Value; + } + if (!TryInterpretPrimitive(out val, operandExpr)) + return false; + result = DoNegateOrNull(val); + return result != null; + } + + if (expr is ConstantExpression constExpr) + { +#if LIGHT_EXPRESSION + if (constExpr.RefField != null) return false; +#endif + result = constExpr.Value; + if (expr.Type == typeof(bool)) + result = (bool)result ? TrueObject : FalseObject; + return true; + } + + if (nodeType == ExpressionType.Default) + { + result = GetZeroDefaultObject(Type.GetTypeCode(expr.Type)); + return true; + } + + if (nodeType == ExpressionType.Convert) + { + var unaryExpr = (UnaryExpression)expr; + var operandExpr = unaryExpr.Operand; + + if (!TryInterpretPrimitive(out result, operandExpr)) + return false; + var exprType = expr.Type; + if (operandExpr.Type != exprType && !exprType.IsAssignableFrom(operandExpr.Type)) + { + var converted = System.Convert.ChangeType(result, exprType); + result = exprType != typeof(bool) ? converted : (bool)converted ? TrueObject : FalseObject; + } + return true; + } + + result = null; + return false; + } + } } - // Helpers targeting the performance. Extensions method names may be a bit funny (non standard), - // in order to prevent conflicts with YOUR helpers with standard names + /// + /// Helpers targeting the performance. Extensions method names may be a bit funny (non standard), + /// in order to prevent conflicts with YOUR helpers with standard names + /// public static class Tools { public static Expression AsExpr(this object obj) => obj as Expression ?? Constant(obj); @@ -8556,10 +8985,6 @@ void PrintPart(Expression part, ref SmallList4 named) false, lineIndent, stripNamespace, printType, indentSpaces, notRecognizedToCode); } - // remove the parens from the simple comparisons and ops between params, variables and constants - if (b.Left.IsParamOrConstantOrDefault() && b.Right.IsParamOrConstantOrDefault()) - avoidParens = true; - sb = !avoidParens ? sb.Append('(') : sb; b.Left.ToCSharpExpression(sb, EnclosedIn.ParensByDefault, ref named, false, lineIndent, stripNamespace, printType, indentSpaces, notRecognizedToCode); diff --git a/src/FastExpressionCompiler/TestTools.cs b/src/FastExpressionCompiler/TestTools.cs index 939c9437..cc3919b2 100644 --- a/src/FastExpressionCompiler/TestTools.cs +++ b/src/FastExpressionCompiler/TestTools.cs @@ -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 = false; static TestTools() { @@ -43,6 +44,8 @@ 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) { @@ -952,6 +955,8 @@ public sealed class TestRun public SmallList Stats; public SmallList Failures; + // todo: @wip put the output under the feature flag + /// Will output the failures while running public void Run(T test, TestTracking tracking = TestTracking.TrackFailedTestsOnly) where T : ITestX { var totalTestCount = TotalTestCount; diff --git a/test/FastExpressionCompiler.Benchmarks/FastExpressionCompiler.Benchmarks.csproj b/test/FastExpressionCompiler.Benchmarks/FastExpressionCompiler.Benchmarks.csproj index 5718c1c2..29a64710 100644 --- a/test/FastExpressionCompiler.Benchmarks/FastExpressionCompiler.Benchmarks.csproj +++ b/test/FastExpressionCompiler.Benchmarks/FastExpressionCompiler.Benchmarks.csproj @@ -1,7 +1,7 @@  - $(LatestSupportedNet) + $(LatestSupportedNet);net8.0 Exe false diff --git a/test/FastExpressionCompiler.Benchmarks/Issue468_Compile_vs_FastCompile.cs b/test/FastExpressionCompiler.Benchmarks/Issue468_Compile_vs_FastCompile.cs new file mode 100644 index 00000000..341398d0 --- /dev/null +++ b/test/FastExpressionCompiler.Benchmarks/Issue468_Compile_vs_FastCompile.cs @@ -0,0 +1,243 @@ +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 | + + +## Steel the same speed with the minimal IL of 2 instructions + +Job=.NET 8.0 Runtime=.NET 8.0 + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | +|------------------- |----------:|----------:|----------:|------:|--------:|-----:|----------:|------------:| +| InvokeCompiled | 0.4647 ns | 0.0321 ns | 0.0268 ns | 1.00 | 0.08 | 1 | - | NA | +| InvokeCompiledFast | 0.9739 ns | 0.0433 ns | 0.0481 ns | 2.10 | 0.15 | 2 | - | NA | + + +## But the Func speed is faster, hmm + +Job=.NET 8.0 Runtime=.NET 8.0 + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | +|--------------- |----------:|----------:|----------:|------:|--------:|-----:|----------:|------------:| +| InvokeCompiled | 0.2685 ns | 0.0210 ns | 0.0186 ns | 1.00 | 0.09 | 2 | - | NA | +| JustFunc | 0.1711 ns | 0.0310 ns | 0.0305 ns | 0.64 | 0.12 | 1 | - | NA | + + +## HERE IS THE REASON: + +FEC creates the DynamicMethod with `owner` param, but System compile uses the different overload without owner and internally with `transparentMethod: true`. +Using this latter (System) overload drastically slows down the compilation but removes the additional branch instruction in the invocation, making a super simple delegates faster. +But for the delegates doing actual/more work, having additional branch instruction is negligible and usually does not show in the invocation performance. + +2x slow: `var method = new DynamicMethod(string.Empty, returnType, closurePlusParamTypes, typeof(ArrayClosure), true);` + ^^^^^^^^^^^^^^^^^^^^ +parity: `var method = new DynamicMethod(string.Empty, returnType, closurePlusParamTypes, true);` + +Job=.NET 8.0 Runtime=.NET 8.0 + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | BranchInstructions/Op | Allocated | Alloc Ratio | +|------------------- |----------:|----------:|----------:|------:|--------:|-----:|----------------------:|----------:|------------:| +| InvokeCompiled | 0.5075 ns | 0.0153 ns | 0.0143 ns | 1.00 | 0.04 | 1 | 1 | - | NA | +| InvokeCompiledFast | 0.5814 ns | 0.0433 ns | 0.0699 ns | 1.15 | 0.14 | 1 | 1 | - | NA | + + +## Not with full eval before Compile the results are funny in the good way + +Job=.NET 8.0 Runtime=.NET 8.0 + +| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | BranchInstructions/Op | Allocated | Alloc Ratio | +|------------------------------- |----------:|----------:|----------:|------:|--------:|-----:|----------------------:|----------:|------------:| +| InvokeCompiled | 0.5071 ns | 0.0289 ns | 0.0242 ns | 1.00 | 0.06 | 2 | 1 | - | NA | +| InvokeCompiledFastWithEvalFlag | 0.0804 ns | 0.0341 ns | 0.0351 ns | 0.16 | 0.07 | 1 | 1 | - | NA | + + +## Fastest so far + +DefaultJob : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + +| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | BranchInstructions/Op | Allocated | Alloc Ratio | +|-------------------------------------- |----------:|----------:|----------:|----------:|------:|--------:|-----:|----------------------:|----------:|------------:| +| InvokeCompiled | 0.5088 ns | 0.0399 ns | 0.0842 ns | 0.4707 ns | 1.02 | 0.22 | 2 | 1 | - | NA | +| InvokeCompiledFast | 0.1105 ns | 0.0360 ns | 0.0799 ns | 0.0689 ns | 0.22 | 0.16 | 1 | 1 | - | NA | +| InvokeCompiledFast_DisableInterpreter | 1.0607 ns | 0.0540 ns | 0.0887 ns | 1.0301 ns | 2.13 | 0.34 | 3 | 2 | - | NA | + +*/ +[MemoryDiagnoser, RankColumn] +[HardwareCounters(HardwareCounter.BranchInstructions)] +// [SimpleJob(RuntimeMoniker.Net90)] +// [SimpleJob(RuntimeMoniker.Net80)] +public class Issue468_InvokeCompiled_vs_InvokeCompiledFast +{ + Func _compiled, _compiledFast, _compiledFast_DisableInterpreter, _justFunc = static () => true; + + [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(); + _compiledFast_DisableInterpreter = expr.CompileFast(flags: CompilerFlags.DisableInterpreter); + } + + [Benchmark(Baseline = true)] + public bool InvokeCompiled() + { + return _compiled(); + } + + [Benchmark] + public bool InvokeCompiledFast() + { + return _compiledFast(); + } + + [Benchmark] + public bool InvokeCompiledFast_DisableInterpreter() + { + return _compiledFast_DisableInterpreter(); + } + + // [Benchmark] + public bool JustFunc() + { + return _justFunc(); + } +} + +/* +## 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 | + + +## After reverting the regression + +| Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | +|-------------------------- |--------- |--------- |----------:|----------:|----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| +| Compiled | .NET 8.0 | .NET 8.0 | 25.093 us | 0.4979 us | 1.1034 us | 1.00 | 0.06 | 2 | 0.6714 | 0.6104 | 4.13 KB | 1.00 | +| CompiledFast | .NET 8.0 | .NET 8.0 | 3.433 us | 0.0680 us | 0.0603 us | 0.14 | 0.01 | 1 | 0.1678 | 0.1526 | 1.12 KB | 0.27 | +| CompiledFast_WithEvalFlag | .NET 8.0 | .NET 8.0 | 3.419 us | 0.0675 us | 0.1409 us | 0.14 | 0.01 | 1 | 0.2365 | 0.2289 | 1.48 KB | 0.36 | +| | | | | | | | | | | | | | +| Compiled | .NET 9.0 | .NET 9.0 | 25.491 us | 0.4667 us | 0.4137 us | 1.00 | 0.02 | 2 | 0.6714 | 0.6104 | 4.13 KB | 1.00 | +| CompiledFast | .NET 9.0 | .NET 9.0 | 3.337 us | 0.0634 us | 0.0593 us | 0.13 | 0.00 | 1 | 0.1793 | 0.1755 | 1.12 KB | 0.27 | +| CompiledFast_WithEvalFlag | .NET 9.0 | .NET 9.0 | 3.198 us | 0.0628 us | 0.0588 us | 0.13 | 0.00 | 1 | 0.2365 | 0.2289 | 1.48 KB | 0.36 | + + +## Funny results after adding eval before compile + +Job=.NET 8.0 Runtime=.NET 8.0 + +| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | +|-------------------------- |------------:|----------:|----------:|------------:|-------:|--------:|-----:|-------:|-------:|----------:|------------:| +| Compiled | 22,507.0 ns | 435.99 ns | 652.57 ns | 22,519.1 ns | 131.40 | 8.03 | 3 | 0.6714 | 0.6409 | 4232 B | 11.02 | +| CompiledFast | 3,051.9 ns | 59.71 ns | 55.86 ns | 3,036.6 ns | 17.82 | 1.01 | 2 | 0.1755 | 0.1678 | 1143 B | 2.98 | +| CompiledFast_WithEvalFlag | 171.8 ns | 3.49 ns | 9.44 ns | 167.6 ns | 1.00 | 0.08 | 1 | 0.0610 | - | 384 B | 1.00 | + + +## Now we're talking (after small interpretator optimizations) + +DefaultJob : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + +| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | +|-------------------------------- |-------------:|-----------:|-----------:|-------------:|-------:|--------:|-----:|-------:|-------:|----------:|------------:| +| Compiled | 22,937.50 ns | 447.883 ns | 784.432 ns | 22,947.67 ns | 230.86 | 14.14 | 3 | 0.6714 | 0.6409 | 4232 B | 88.17 | +| CompiledFast | 99.62 ns | 2.044 ns | 5.275 ns | 97.03 ns | 1.00 | 0.07 | 1 | 0.0076 | - | 48 B | 1.00 | +| CompiledFast_DisableInterpreter | 3,010.37 ns | 60.174 ns | 91.893 ns | 3,010.03 ns | 30.30 | 1.80 | 2 | 0.1755 | 0.1678 | 1143 B | 23.81 | +*/ +[MemoryDiagnoser, RankColumn] +// [SimpleJob(RuntimeMoniker.Net90)] +// [SimpleJob(RuntimeMoniker.Net80)] +public class Issue468_Compile_vs_FastCompile +{ + Expression> _expr; + + [GlobalSetup] + public void Setup() + { + _expr = IssueTests.Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET.CreateExpression(); + } + + [Benchmark] + public object Compiled() + { + return _expr.Compile(); + } + + [Benchmark(Baseline = true)] + public object CompiledFast() + { + return _expr.CompileFast(); + } + + [Benchmark] + public object CompiledFast_DisableInterpreter() + { + return _expr.CompileFast(flags: CompilerFlags.DisableInterpreter); + } +} + +[MemoryDiagnoser, RankColumn] +// [SimpleJob(RuntimeMoniker.Net90)] +// [SimpleJob(RuntimeMoniker.Net80)] +public class Issue468_Eval_Optimization +{ + Expression> _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 Baseline() + // { + // return ExpressionCompiler.Interpreter.TryEvalPrimitive_OLD(out var result, _expr) ? result : null; + // } + + [Benchmark] + public object Optimized() + { + return ExpressionCompiler.Interpreter.TryInterpretPrimitive(out var result, _expr) ? result : null; + } +} diff --git a/test/FastExpressionCompiler.Benchmarks/Program.cs b/test/FastExpressionCompiler.Benchmarks/Program.cs index 65fab977..2a68e389 100644 --- a/test/FastExpressionCompiler.Benchmarks/Program.cs +++ b/test/FastExpressionCompiler.Benchmarks/Program.cs @@ -20,11 +20,14 @@ public static void Main() // BenchmarkRunner.Run(); // not included in README.md, may be it needs to // BenchmarkRunner.Run(); // not included in README.md, may be it needs to - BenchmarkRunner.Run(); - BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); //-------------------------------------------- + // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); diff --git a/test/FastExpressionCompiler.IssueTests/Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET.cs b/test/FastExpressionCompiler.IssueTests/Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET.cs new file mode 100644 index 00000000..1d8e7881 --- /dev/null +++ b/test/FastExpressionCompiler.IssueTests/Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET.cs @@ -0,0 +1,89 @@ +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 the benchmarking + public static Expression> CreateExpression( +#if LIGHT_EXPRESSION + bool addClosure = false +#endif + ) + { + var e = new Expression[11]; // the unique expressions + var expr = Lambda>( + 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(); + // outputs: + var @cs = (Func)(() => //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()); + + var ffe = expr.CompileFast(false, CompilerFlags.DisableInterpreter); + ffe.PrintIL(); + t.IsTrue(ffe()); + } + + 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 + } +} \ No newline at end of file diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 9a2840bc..8c6f7126 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -9,7 +9,10 @@ public class Program { public static void Main() { - new Issue55_CompileFast_crash_with_ref_parameter().Run(); + var t = new LightExpression.TestRun(); + t.Run(new LightExpression.IssueTests.Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET()); + + // new Issue55_CompileFast_crash_with_ref_parameter().Run(); // todo: @wip add to FEC, check the possibility of the increment compilation and the artifacts reusability // new LightExpression.UnitTests.ConstantAndConversionTests().Run();