diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 70f7f94a..4a0fdf33 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -122,9 +122,9 @@ public static TDelegate CompileFast(this LambdaExpression lambdaExpr, (TDelegate)(TryCompileBoundToFirstClosureParam( typeof(TDelegate) == typeof(Delegate) ? lambdaExpr.Type : typeof(TDelegate), lambdaExpr.Body, #if LIGHT_EXPRESSION - lambdaExpr, RentOrNewClosureTypeToParamTypes(lambdaExpr), + lambdaExpr, #else - lambdaExpr.Parameters, RentOrNewClosureTypeToParamTypes(lambdaExpr.Parameters), + lambdaExpr.Parameters, #endif lambdaExpr.ReturnType, flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys())); @@ -163,9 +163,9 @@ public static bool CompileFastToIL(this LambdaExpression lambdaExpr, ILGenerator public static Delegate CompileFast(this LambdaExpression lambdaExpr, bool ifFastFailedReturnNull = false, CompilerFlags flags = CompilerFlags.Default) => (Delegate)TryCompileBoundToFirstClosureParam(lambdaExpr.Type, lambdaExpr.Body, #if LIGHT_EXPRESSION - lambdaExpr, RentOrNewClosureTypeToParamTypes(lambdaExpr), + lambdaExpr, #else - lambdaExpr.Parameters, RentOrNewClosureTypeToParamTypes(lambdaExpr.Parameters), + lambdaExpr.Parameters, #endif lambdaExpr.ReturnType, flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); @@ -215,7 +215,7 @@ public static Func CompileFast(this Expression> lambdaExpr, bool i #else lambdaExpr.Parameters, #endif - _closureAsASingleParamType, typeof(R), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); + typeof(R), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. public static Func CompileFast(this Expression> lambdaExpr, @@ -226,7 +226,7 @@ public static Func CompileFast(this Expression> lambda #else lambdaExpr.Parameters, #endif - new[] { typeof(ArrayClosure), typeof(T1) }, typeof(R), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); + typeof(R), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to TDelegate type. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. public static Func CompileFast(this Expression> lambdaExpr, @@ -237,7 +237,6 @@ public static Func CompileFast(this ExpressionCompiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. @@ -249,7 +248,7 @@ public static Func CompileFast( #else lambdaExpr.Parameters, #endif - new[] { typeof(ArrayClosure), typeof(T1), typeof(T2), typeof(T3) }, typeof(R), flags) + typeof(R), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to TDelegate type. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. @@ -261,7 +260,7 @@ public static Func CompileFast( #else lambdaExpr.Parameters, #endif - new[] { typeof(ArrayClosure), typeof(T1), typeof(T2), typeof(T3), typeof(T4) }, typeof(R), flags) + typeof(R), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. @@ -273,7 +272,7 @@ public static Func CompileFast( #else lambdaExpr.Parameters, #endif - new[] { typeof(ArrayClosure), typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5) }, typeof(R), flags) + typeof(R), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. @@ -285,7 +284,7 @@ public static Func CompileFastCompiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. @@ -296,7 +295,7 @@ public static Action CompileFast(this Expression lambdaExpr, bool ifFast #else lambdaExpr.Parameters, #endif - _closureAsASingleParamType, typeof(void), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); + typeof(void), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. public static Action CompileFast(this Expression> lambdaExpr, @@ -307,7 +306,7 @@ public static Action CompileFast(this Expression> lambdaExpr, #else lambdaExpr.Parameters, #endif - new[] { typeof(ArrayClosure), typeof(T1) }, typeof(void), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); + typeof(void), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. public static Action CompileFast(this Expression> lambdaExpr, @@ -318,7 +317,7 @@ public static Action CompileFast(this Expression> #else lambdaExpr.Parameters, #endif - new[] { typeof(ArrayClosure), typeof(T1), typeof(T2) }, typeof(void), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); + typeof(void), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. public static Action CompileFast(this Expression> lambdaExpr, @@ -329,7 +328,7 @@ public static Action CompileFast(this ExpressionCompiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. @@ -341,7 +340,7 @@ public static Action CompileFast( #else lambdaExpr.Parameters, #endif - new[] { typeof(ArrayClosure), typeof(T1), typeof(T2), typeof(T3), typeof(T4) }, typeof(void), flags) + typeof(void), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. @@ -353,7 +352,7 @@ public static Action CompileFast( #else lambdaExpr.Parameters, #endif - new[] { typeof(ArrayClosure), typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5) }, typeof(void), flags) + typeof(void), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); /// Compiles lambda expression to delegate. Use ifFastFailedReturnNull parameter to Not fallback to Expression.Compile, useful for testing. @@ -365,7 +364,7 @@ public static Action CompileFast #else lambdaExpr.Parameters, #endif - new[] { typeof(ArrayClosure), typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6) }, typeof(void), flags) + typeof(void), flags) ?? (ifFastFailedReturnNull ? null : lambdaExpr.CompileSys()); #endregion @@ -375,9 +374,9 @@ public static TDelegate TryCompile(this LambdaExpression lambdaExpr, where TDelegate : class => (TDelegate)TryCompileBoundToFirstClosureParam(typeof(TDelegate) == typeof(Delegate) ? lambdaExpr.Type : typeof(TDelegate), lambdaExpr.Body, #if LIGHT_EXPRESSION - lambdaExpr, RentOrNewClosureTypeToParamTypes(lambdaExpr), + lambdaExpr, #else - lambdaExpr.Parameters, RentOrNewClosureTypeToParamTypes(lambdaExpr.Parameters), + lambdaExpr.Parameters, #endif lambdaExpr.ReturnType, flags); @@ -408,14 +407,15 @@ public static TDelegate TryCompileWithPreCreatedClosure(this LambdaEx internal static TDelegate TryCompileWithPreCreatedClosure( this LambdaExpression lambdaExpr, ref ClosureInfo closureInfo, CompilerFlags flags) where TDelegate : class { + var closureType = typeof(ArrayClosure); #if LIGHT_EXPRESSION - var closurePlusParamTypes = RentOrNewClosureTypeToParamTypes(lambdaExpr); + var closurePlusParamTypes = RentOrNewClosureTypePlusParamTypes(closureType, lambdaExpr); #else - var closurePlusParamTypes = RentOrNewClosureTypeToParamTypes(lambdaExpr.Parameters); + var closurePlusParamTypes = RentOrNewClosureTypePlusParamTypes(closureType, lambdaExpr.Parameters); #endif - var method = new DynamicMethod(string.Empty, lambdaExpr.ReturnType, closurePlusParamTypes, - typeof(ExpressionCompiler), skipVisibility: true); + var method = new DynamicMethod(string.Empty, lambdaExpr.ReturnType, closurePlusParamTypes, closureType, true); + closureInfo.ConstantsAndNestedLambdasField = ArrayClosure.ConstantsAndNestedLambdasField; var il = method.GetILGenerator(); EmittingVisitor.EmitLoadConstantsAndNestedLambdasIntoVars(il, ref closureInfo); @@ -433,7 +433,7 @@ internal static TDelegate TryCompileWithPreCreatedClosure( var delegateType = typeof(TDelegate) != typeof(Delegate) ? typeof(TDelegate) : lambdaExpr.Type; var @delegate = (TDelegate)(object)method.CreateDelegate(delegateType, new ArrayClosure(closureInfo.Constants.Items)); - ReturnClosureTypeToParamTypesToPool(closurePlusParamTypes); + ReturnTypeArrayToPool(closurePlusParamTypes); return @delegate; } @@ -442,13 +442,13 @@ public static TDelegate TryCompileWithoutClosure(this LambdaExpressio CompilerFlags flags = CompilerFlags.Default) where TDelegate : class { var closureInfo = new ClosureInfo(ClosureStatus.UserProvided); + var closureType = typeof(EmptyClosure); #if LIGHT_EXPRESSION - var closurePlusParamTypes = RentOrNewClosureTypeToParamTypes(lambdaExpr); + var closurePlusParamTypes = RentOrNewClosureTypePlusParamTypes(closureType, lambdaExpr); #else - var closurePlusParamTypes = RentOrNewClosureTypeToParamTypes(lambdaExpr.Parameters); + var closurePlusParamTypes = RentOrNewClosureTypePlusParamTypes(closureType, lambdaExpr.Parameters); #endif - var method = new DynamicMethod(string.Empty, lambdaExpr.ReturnType, closurePlusParamTypes, typeof(ArrayClosure), - skipVisibility: true); + var method = new DynamicMethod(string.Empty, lambdaExpr.ReturnType, closurePlusParamTypes, closureType, true); var il = method.GetILGenerator(); if (!EmittingVisitor.TryEmit(lambdaExpr.Body, @@ -463,31 +463,31 @@ public static TDelegate TryCompileWithoutClosure(this LambdaExpressio il.Demit(OpCodes.Ret); var delegateType = typeof(TDelegate) != typeof(Delegate) ? typeof(TDelegate) : lambdaExpr.Type; - var @delegate = (TDelegate)(object)method.CreateDelegate(delegateType, EmptyArrayClosure); - ReturnClosureTypeToParamTypesToPool(closurePlusParamTypes); + var @delegate = (TDelegate)(object)method.CreateDelegate(delegateType, EmptyClosure.Instance); + ReturnTypeArrayToPool(closurePlusParamTypes); return @delegate; } - private static Delegate CompileNoArgsNew(ConstructorInfo ctor, Type delegateType, Type[] closurePlusParamTypes, Type returnType) + private static Delegate CompileNoArgsNew(ConstructorInfo ctor, Type delegateType, Type returnType) { - var method = new DynamicMethod(string.Empty, returnType, closurePlusParamTypes, typeof(ArrayClosure), true); + var method = new DynamicMethod(string.Empty, returnType, Tools.Empty(), true); var il = method.GetILGenerator(16); // 16 is enough for maximum of 3 possible ops il.Demit(OpCodes.Newobj, ctor); if (returnType == typeof(void)) il.Demit(OpCodes.Pop); il.Demit(OpCodes.Ret); - return method.CreateDelegate(delegateType, EmptyArrayClosure); + return method.CreateDelegate(delegateType, null); } #if LIGHT_EXPRESSION internal static object TryCompileBoundToFirstClosureParam(Type delegateType, Expression bodyExpr, IParameterProvider paramExprs, - Type[] closurePlusParamTypes, Type returnType, CompilerFlags flags) + Type returnType, CompilerFlags flags) { if (bodyExpr is NoArgsNewClassIntrinsicExpression newNoArgs) - return CompileNoArgsNew(newNoArgs.Constructor, delegateType, closurePlusParamTypes, returnType); + return CompileNoArgsNew(newNoArgs.Constructor, delegateType, returnType); #else internal static object TryCompileBoundToFirstClosureParam(Type delegateType, Expression bodyExpr, IReadOnlyList paramExprs, - Type[] closurePlusParamTypes, Type returnType, CompilerFlags flags) + Type returnType, CompilerFlags flags) { #endif // The method collects the info from the all nested lambdas deep down up-front and de-duplicates the lambdas as well. @@ -495,28 +495,43 @@ internal static object TryCompileBoundToFirstClosureParam(Type delegateType, Exp if (!TryCollectBoundConstants(ref closureInfo, bodyExpr, paramExprs, null, ref closureInfo.NestedLambdas, flags)) return null; - ArrayClosure closure; + var constantsAndNestedLambdas = (closureInfo.Status & ClosureStatus.HasClosure) == 0 ? null + : closureInfo.GetArrayOfConstantsAndNestedLambdas(); + + object closure = null; + Type closureType = null; + FieldInfo constantsField = null; if ((flags & CompilerFlags.EnableDelegateDebugInfo) == 0) { - closure = (closureInfo.Status & ClosureStatus.HasClosure) == 0 - ? EmptyArrayClosure - : new ArrayClosure(closureInfo.GetArrayOfConstantsAndNestedLambdas()); + if (constantsAndNestedLambdas == null) + { + closure = EmptyClosure.Instance; + closureType = typeof(EmptyClosure); + } + else + { + closure = new ArrayClosure(constantsAndNestedLambdas); + closureType = typeof(ArrayClosure); + constantsField = ArrayClosure.ConstantsAndNestedLambdasField; + } } else - { // todo: @feature add the debug info to the nested lambdas! + { // todo: @feature add the debug info into the nested lambdas! var debugExpr = Lambda(delegateType, bodyExpr, paramExprs?.ToReadOnlyList() ?? Tools.Empty()); - var constantsAndNestedLambdas = (closureInfo.Status & ClosureStatus.HasClosure) == 0 - ? null - : closureInfo.GetArrayOfConstantsAndNestedLambdas(); closure = new DebugArrayClosure(constantsAndNestedLambdas, debugExpr); + closureType = typeof(DebugArrayClosure); + constantsField = DebugArrayClosure.ConstantsAndNestedLambdasField; } - var method = new DynamicMethod(string.Empty, returnType, closurePlusParamTypes, typeof(ArrayClosure), true); + var paramTypes = RentOrNewClosureTypePlusParamTypes(closureType, paramExprs); + + var method = new DynamicMethod(string.Empty, returnType, paramTypes, closureType, true); // todo: @perf can we just count the Expressions in the TryCollect phase and use it as N * 4 or something? var il = method.GetILGenerator(); - if (closure.ConstantsAndNestedLambdas != null) + closureInfo.ConstantsAndNestedLambdasField = constantsField; + if (constantsAndNestedLambdas != null) EmittingVisitor.EmitLoadConstantsAndNestedLambdasIntoVars(il, ref closureInfo); var parent = returnType == typeof(void) ? ParentFlags.IgnoreResult : ParentFlags.LambdaCall; @@ -527,55 +542,44 @@ internal static object TryCompileBoundToFirstClosureParam(Type delegateType, Exp return null; il.Demit(OpCodes.Ret); - return method.CreateDelegate(delegateType, closure); + var result = method.CreateDelegate(delegateType, closure); + + ReturnTypeArrayToPool(paramTypes); + return result; } - 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? + private static readonly Type[][] _typeArrayPool = new Type[8][]; // todo: @perf @mem could we use this for other Type arrays? #if LIGHT_EXPRESSION - private static Type[] RentOrNewClosureTypeToParamTypes(IParameterProvider paramExprs) + private static Type[] RentOrNewClosureTypePlusParamTypes(Type closureType, IParameterProvider paramExprs) { - var count = paramExprs.ParameterCount; + var paramCount = paramExprs.ParameterCount; #else - private static Type[] RentOrNewClosureTypeToParamTypes(IReadOnlyList paramExprs) + private static Type[] RentOrNewClosureTypePlusParamTypes(Type closureType, IReadOnlyList paramExprs) { - var count = paramExprs.Count; + var paramCount = paramExprs.Count; #endif - if (count == 0) - return _closureAsASingleParamType; - - if (count < 8) - { - var pooledClosureAndParamTypes = Interlocked.Exchange(ref _closureTypePlusParamTypesPool[count], null); - if (pooledClosureAndParamTypes != null) - { - for (var i = 0; i < count; i++) - { - var parameterExpr = paramExprs.GetParameter(i); // todo: @perf can we avoid calling virtual GetParameter() and maybe use intrinsic with NoByRef? - pooledClosureAndParamTypes[i + 1] = parameterExpr.IsByRef ? parameterExpr.Type.MakeByRefType() : parameterExpr.Type; - } - return pooledClosureAndParamTypes; - } - } + var closurePlusParamTypes = paramCount < 8 + ? Interlocked.Exchange(ref _typeArrayPool[paramCount], null) ?? new Type[paramCount + 1] + : new Type[paramCount + 1]; + Debug.Assert(closurePlusParamTypes.Length >= 1); - // todo: @perf the code maybe simplified and then will be the candidate for the inlining - var closureAndParamTypes = new Type[count + 1]; - closureAndParamTypes[0] = typeof(ArrayClosure); - for (var i = 0; i < count; i++) + closurePlusParamTypes[0] = closureType; + for (var i = 1; i < (uint)closurePlusParamTypes.Length; ++i) { - var parameterExpr = paramExprs.GetParameter(i); - closureAndParamTypes[i + 1] = parameterExpr.IsByRef ? parameterExpr.Type.MakeByRefType() : parameterExpr.Type; + // todo: @perf can we avoid calling virtual GetParameter() and maybe use intrinsic with NoByRef? + var paramExpr = paramExprs.GetParameter(i - 1); + closurePlusParamTypes[i] = paramExpr.IsByRef ? paramExpr.Type.MakeByRefType() : paramExpr.Type; } - return closureAndParamTypes; + return closurePlusParamTypes; } [MethodImpl((MethodImplOptions)256)] - private static void ReturnClosureTypeToParamTypesToPool(Type[] closurePlusParamTypes) + private static void ReturnTypeArrayToPool(Type[] paramTypes) { - var paramCount = closurePlusParamTypes.Length - 1; - if (paramCount != 0 && paramCount < 8) - Interlocked.Exchange(ref _closureTypePlusParamTypesPool[paramCount], closurePlusParamTypes); // todo: @perf we don't need the Interlocked here + var paramCount = paramTypes.Length - 1; + if (paramCount < 8) + Interlocked.Exchange(ref _typeArrayPool[paramCount], paramTypes); // todo: @perf we don't need the Interlocked here } #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -698,6 +702,9 @@ public struct ClosureInfo /// The nested lambdas and their info public SmallList NestedLambdas; + /// The constant array field + public FieldInfo ConstantsAndNestedLambdasField; + /// Populates the info public ClosureInfo(ClosureStatus status) { @@ -895,32 +902,36 @@ private static Label GetOrDefineLabel(ref this LabelInfo label, ILGenerator il) return label.Label; } - public static readonly ArrayClosure EmptyArrayClosure = new ArrayClosure(null); - - public static FieldInfo ArrayClosureArrayField = - typeof(ArrayClosure).GetField(nameof(ArrayClosure.ConstantsAndNestedLambdas)); - - public static FieldInfo ArrayClosureWithNonPassedParamsField = - typeof(ArrayClosureWithNonPassedParams).GetField(nameof(ArrayClosureWithNonPassedParams.NonPassedParams)); - private static ConstructorInfo[] _nonPassedParamsArrayClosureCtors = typeof(ArrayClosureWithNonPassedParams).GetConstructors(); public static ConstructorInfo ArrayClosureWithNonPassedParamsAndConstantsCtor = _nonPassedParamsArrayClosureCtors[0]; - public static ConstructorInfo ArrayClosureWithNonPassedParamsCtor = _nonPassedParamsArrayClosureCtors[1]; + public static ConstructorInfo ArrayClosureWithNonPassedParamsCtor = _nonPassedParamsArrayClosureCtors[1]; public static Result NotSupported_RuntimeVariables { get; private set; } - public static Result NotSupported_RuntimeVariables { get; private set; } + public sealed class EmptyClosure + { + public static readonly EmptyClosure Instance = new EmptyClosure(); + private EmptyClosure() { } + } - public class ArrayClosure + public sealed class ArrayClosure { + public static FieldInfo ConstantsAndNestedLambdasField = + typeof(ArrayClosure).GetField(nameof(ConstantsAndNestedLambdas)); + // todo: @feature split into two to reduce copying public readonly object[] ConstantsAndNestedLambdas; public ArrayClosure(object[] constantsAndNestedLambdas) => ConstantsAndNestedLambdas = constantsAndNestedLambdas; } [RequiresUnreferencedCode(Trimming.Message)] - public sealed class DebugArrayClosure : ArrayClosure, IDelegateDebugInfo + public sealed class DebugArrayClosure : IDelegateDebugInfo { + public static FieldInfo ConstantsAndNestedLambdasField = + typeof(ArrayClosure).GetField(nameof(ConstantsAndNestedLambdas)); + + public readonly object[] ConstantsAndNestedLambdas; + public LambdaExpression Expression { get; internal set; } private readonly Lazy _expressionString; @@ -929,8 +940,8 @@ public sealed class DebugArrayClosure : ArrayClosure, IDelegateDebugInfo private readonly Lazy _csharpString; public string CSharpString => _csharpString.Value; public DebugArrayClosure(object[] constantsAndNestedLambdas, LambdaExpression expr) - : base(constantsAndNestedLambdas) { + ConstantsAndNestedLambdas = constantsAndNestedLambdas; Expression = expr; _expressionString = new Lazy(() => Expression?.ToExpressionString() ?? ""); _csharpString = new Lazy(() => Expression?.ToCSharpString() ?? ""); @@ -984,13 +995,22 @@ public static IEnumerable EnumerateDebugConstantsAndNestedLambdas(this D } // todo: @perf better to move the case with no constants to another class OR we can reuse ArrayClosure but now ConstantsAndNestedLambdas will hold NonPassedParams - public sealed class ArrayClosureWithNonPassedParams : ArrayClosure + public sealed class ArrayClosureWithNonPassedParams { + public static FieldInfo ConstantsAndNestedLambdasField = + typeof(ArrayClosureWithNonPassedParams).GetField(nameof(ConstantsAndNestedLambdas)); + public static FieldInfo NonPassedParamsField = + typeof(ArrayClosureWithNonPassedParams).GetField(nameof(NonPassedParams)); + + public readonly object[] ConstantsAndNestedLambdas; public readonly object[] NonPassedParams; - public ArrayClosureWithNonPassedParams(object[] nonPassedParams, object[] constantsAndNestedLambdas) : base(constantsAndNestedLambdas) => + public ArrayClosureWithNonPassedParams(object[] nonPassedParams, object[] constantsAndNestedLambdas) + { + ConstantsAndNestedLambdas = constantsAndNestedLambdas; NonPassedParams = nonPassedParams; - // todo: @perf optimize for this case - public ArrayClosureWithNonPassedParams(object[] nonPassedParams) : base(null) => + } + // todo: @perf optimize for this case, or may be just reuse the ArrayClosure because those are just an objects + public ArrayClosureWithNonPassedParams(object[] nonPassedParams) => NonPassedParams = nonPassedParams; } @@ -1734,9 +1754,7 @@ private static bool TryCompileNestedLambda(ref ClosureInfo nestedClosureInfo, Ne if (nestedLambdaBody is NoArgsNewClassIntrinsicExpression newNoArgs) { - var paramTypes = RentOrNewClosureTypeToParamTypes(nestedLambdaParamExprs); - nestedLambdaInfo.Lambda = CompileNoArgsNew(newNoArgs.Constructor, nestedLambdaExpr.Type, paramTypes, nestedReturnType); - ReturnClosureTypeToParamTypesToPool(paramTypes); + nestedLambdaInfo.Lambda = CompileNoArgsNew(newNoArgs.Constructor, nestedLambdaExpr.Type, nestedReturnType); return true; } #else @@ -1748,18 +1766,36 @@ private static bool TryCompileNestedLambda(ref ClosureInfo nestedClosureInfo, Ne var nestedConstsAndLambdas = nestedClosureInfo.GetArrayOfConstantsAndNestedLambdas(); - ArrayClosure nestedLambdaClosure = null; + object nestedLambdaClosure = null; + Type closureType = null; + FieldInfo closureConstantField = null; var hasNonPassedParameters = nestedLambdaInfo.NonPassedParameters.Count != 0; - if (!hasNonPassedParameters) - nestedLambdaClosure = (nestedClosureInfo.Status & ClosureStatus.HasClosure) == 0 - ? EmptyArrayClosure - : new ArrayClosure(nestedConstsAndLambdas); + if (hasNonPassedParameters) + { + closureType = typeof(ArrayClosureWithNonPassedParams); + closureConstantField = ArrayClosureWithNonPassedParams.ConstantsAndNestedLambdasField; + } + else + { + if (nestedConstsAndLambdas != null) + { + nestedLambdaClosure = new ArrayClosure(nestedConstsAndLambdas); + closureType = typeof(ArrayClosure); + closureConstantField = ArrayClosure.ConstantsAndNestedLambdasField; + } + else + { + closureType = typeof(EmptyClosure); + nestedLambdaClosure = EmptyClosure.Instance; + } + } - var closurePlusParamTypes = RentOrNewClosureTypeToParamTypes(nestedLambdaParamExprs); + var closurePlusParamTypes = RentOrNewClosureTypePlusParamTypes(closureType, nestedLambdaParamExprs); - var method = new DynamicMethod(string.Empty, nestedReturnType, closurePlusParamTypes, typeof(ArrayClosure), true); + var method = new DynamicMethod(string.Empty, nestedReturnType, closurePlusParamTypes, closureType, true); var il = method.GetILGenerator(); + nestedClosureInfo.ConstantsAndNestedLambdasField = closureConstantField; if (nestedConstsAndLambdas != null) EmittingVisitor.EmitLoadConstantsAndNestedLambdasIntoVars(il, ref nestedClosureInfo); @@ -1781,7 +1817,7 @@ private static bool TryCompileNestedLambda(ref ClosureInfo nestedClosureInfo, Ne : nestedConstsAndLambdas == null ? new NestedLambdaForNonPassedParams(nestedLambda) : new NestedLambdaForNonPassedParamsWithConstants(nestedLambda, nestedConstsAndLambdas); - ReturnClosureTypeToParamTypesToPool(closurePlusParamTypes); + ReturnTypeArrayToPool(closurePlusParamTypes); return true; } @@ -2941,7 +2977,7 @@ public static bool TryEmitParameter(ParameterExpression paramExpr, // Load non-passed argument from Closure - closure object is always a first argument il.Demit(OpCodes.Ldarg_0); - il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); + il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParams.NonPassedParamsField); EmitLoadConstantInt(il, nonPassedParamIndex); il.Demit(OpCodes.Ldelem_Ref); return il.TryEmitUnboxOf(paramType); @@ -2963,7 +2999,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 @@ -2979,7 +3016,7 @@ public static bool TryEmitNonByRefNonValueTypeParameter(ParameterExpression para // Load non-passed argument from Closure - closure object is always a first argument il.Demit(OpCodes.Ldarg_0); - il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); + il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParams.NonPassedParamsField); EmitLoadConstantInt(il, nonPassedParamIndex); il.Demit(OpCodes.Ldelem_Ref); return true; @@ -3645,7 +3682,7 @@ internal static void EmitLoadConstantsAndNestedLambdasIntoVars(ILGenerator il, r // todo: @perf load the field to `var` only if the constants are more than 1 // Load constants array field from Closure and store it into the variable il.Demit(OpCodes.Ldarg_0); - il.Demit(OpCodes.Ldfld, ArrayClosureArrayField); + il.Demit(OpCodes.Ldfld, closure.ConstantsAndNestedLambdasField); EmitStoreLocalVariable(il, il.GetNextLocalVarIndex(typeof(object[]))); // always does Stloc_0, because it is done at start of the lambda emit // important that the constant will contain the nested lambdas as well in the same array after the actual constants, @@ -4570,9 +4607,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) @@ -4615,7 +4651,7 @@ private static bool TryEmitAssignToParameterOrVariable( // load array field and param item index il.Demit(OpCodes.Ldarg_0); // load closure as it is always an argument zero - il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); + il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParams.NonPassedParamsField); EmitLoadConstantInt(il, nonPassedParamIndex); EmitLoadLocalVariable(il, rightVar); @@ -4636,7 +4672,7 @@ private static bool TryEmitAssignToParameterOrVariable( if (resultVar != -1 & isPost) { il.Demit(OpCodes.Ldarg_0); // load closure as it is always an argument zero - il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); + il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParams.NonPassedParamsField); EmitLoadConstantInt(il, nonPassedParamIndex); il.Demit(OpCodes.Ldelem_Ref); // load the variable from array if (exprType.IsValueType) @@ -4656,7 +4692,7 @@ private static bool TryEmitAssignToParameterOrVariable( EmitStoreLocalVariable(il, arithmeticResultVar); il.Demit(OpCodes.Ldarg_0); // load closure as it is always an argument zero - il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); + il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParams.NonPassedParamsField); EmitLoadConstantInt(il, nonPassedParamIndex); EmitLoadLocalVariable(il, arithmeticResultVar); @@ -5104,7 +5140,7 @@ private static bool TryEmitNestedLambda(LambdaExpression lambdaExpr, IReadOnlyLi // Load the parameter from outer closure `Items` array il.Demit(OpCodes.Ldarg_0); // closure is always a first argument - il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); + il.Demit(OpCodes.Ldfld, ArrayClosureWithNonPassedParams.NonPassedParamsField); EmitLoadConstantInt(il, outerNonPassedParamIndex); il.Demit(OpCodes.Ldelem_Ref); } @@ -5421,6 +5457,26 @@ private static MethodInfo FindBinaryOperandMethod( return null; } + // todo: @wip #468 + internal static bool TryReduceComparisonByEvalConstantsAndArithmetics(out bool result, Expression left, Expression right, bool isEquality) + { + if (left is ConstantExpression lc && lc.Type.IsPrimitive && + right is ConstantExpression rc && rc.Type.IsPrimitive +#if LIGHT_EXPRESSION + // exclude the ref + && lc is not ConstantRefExpression && rc is not ConstantRefExpression +#endif + ) + { + + result = lc.Value.Equals(rc.Value) && !isEquality; + return true; + } + + result = false; + return false; + } + private static bool TryEmitComparison( Expression left, Expression right, Type exprType, ExpressionType nodeType, #if LIGHT_EXPRESSION @@ -5453,6 +5509,13 @@ private static bool TryEmitComparison( var isEqualityOp = nodeType == ExpressionType.Equal | nodeType == ExpressionType.NotEqual; if (isEqualityOp) { + // if (leftType.IsPrimitive && + // TryReduceComparisonByEvalConstantsAndArithmetics(out bool result, left, right, nodeType == ExpressionType.Equal)) + // { + // il.Demit((bool)result ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + // return il.EmitPopIfIgnoreResult(parent); + // } + if (leftIsNullable & rightIsNull) { if (!TryEmit(left, paramExprs, il, ref closure, setup, operandParent)) @@ -7038,8 +7101,8 @@ static ILGeneratorHacks() var postIncMethod = typeof(ILGeneratorHacks).GetTypeInfo().GetDeclaredMethod(nameof(PostInc)); var efficientMethod = new DynamicMethod(string.Empty, - typeof(int), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(ILGenerator), typeof(Type) }, - typeof(ExpressionCompiler.ArrayClosure), skipVisibility: true); + typeof(int), new[] { typeof(ExpressionCompiler.EmptyClosure), typeof(ILGenerator), typeof(Type) }, + typeof(ExpressionCompiler.EmptyClosure), skipVisibility: true); var il = efficientMethod.GetILGenerator(); // emitting `il.m_localSignature.AddArgument(type);` @@ -7057,7 +7120,7 @@ static ILGeneratorHacks() il.Emit(OpCodes.Ret); _getNextLocalVarIndex = (Func)efficientMethod.CreateDelegate( - typeof(Func), ExpressionCompiler.EmptyArrayClosure); + typeof(Func), ExpressionCompiler.EmptyClosure.Instance); // todo: @perf do batch Emit by manually calling `EnsureCapacity` once then `InternalEmit` multiple times // todo: @perf Replace the `Emit(opcode, int)` with the more specialized `Emit(opcode)`, `Emit(opcode, byte)` or `Emit(opcode, short)` @@ -8556,10 +8619,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..0c893203 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 = true; // todo: @wip make it false in the release build static TestTools() { @@ -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) { @@ -952,6 +954,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..4bfdbad8 --- /dev/null +++ b/test/FastExpressionCompiler.Benchmarks/Issue468_Compile_vs_FastCompile.cs @@ -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 _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> _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(); + } +} diff --git a/test/FastExpressionCompiler.Benchmarks/Program.cs b/test/FastExpressionCompiler.Benchmarks/Program.cs index 65fab977..f7998e4b 100644 --- a/test/FastExpressionCompiler.Benchmarks/Program.cs +++ b/test/FastExpressionCompiler.Benchmarks/Program.cs @@ -20,11 +20,13 @@ 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(); diff --git a/test/FastExpressionCompiler.IssueTests/EmitHacksTest.cs b/test/FastExpressionCompiler.IssueTests/EmitHacksTest.cs index 4988da35..ebc41f91 100644 --- a/test/FastExpressionCompiler.IssueTests/EmitHacksTest.cs +++ b/test/FastExpressionCompiler.IssueTests/EmitHacksTest.cs @@ -42,7 +42,7 @@ public void DynamicMethod_Emit_Hack() private static Func> GetScopeTokens() { var dynMethod = new DynamicMethod(string.Empty, - typeof(IList), new[] { typeof(ExpressionCompiler.ArrayClosure), typeof(ILGenerator) }, + typeof(IList), new[] { typeof(ExpressionCompiler.EmptyClosure), typeof(ILGenerator) }, typeof(ExpressionCompiler), skipVisibility: true); var il = dynMethod.GetILGenerator(); @@ -51,7 +51,7 @@ private static Func> GetScopeTokens() il.Emit(OpCodes.Ldfld, mTokensField); il.Emit(OpCodes.Ret); - return (Func>)dynMethod.CreateDelegate(typeof(Func>), ExpressionCompiler.EmptyArrayClosure); + return (Func>)dynMethod.CreateDelegate(typeof(Func>), ExpressionCompiler.EmptyClosure.Instance); } static readonly Func> getScopeTokens = GetScopeTokens(); @@ -60,7 +60,7 @@ private static Func> GetScopeTokens() private static GetFieldRefDelegate CreateFieldAccessor(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(); @@ -83,7 +83,7 @@ public static Func 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); @@ -127,7 +127,7 @@ public static Func Get_DynamicMethod_Emit_Hack() mILStream[mLength++] = (byte)OpCodes.Ret.Value; updateStackSizeDelegate(il, OpCodes.Ret, 0); - return (Func)dynMethod.CreateDelegate(typeof(Func), ExpressionCompiler.EmptyArrayClosure); + return (Func)dynMethod.CreateDelegate(typeof(Func), ExpressionCompiler.EmptyClosure.Instance); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -153,7 +153,7 @@ public void DynamicMethod_Emit_OpCodes_Call() public static Func 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(); @@ -164,7 +164,7 @@ public static Func Get_DynamicMethod_Emit_OpCodes_Call() // il.Emit(OpCodes.Call, MethodStaticNoArgs); il.Emit(OpCodes.Ret); - return (Func)dynMethod.CreateDelegate(typeof(Func), ExpressionCompiler.EmptyArrayClosure); + return (Func)dynMethod.CreateDelegate(typeof(Func), ExpressionCompiler.EmptyClosure.Instance); } @@ -186,7 +186,7 @@ public void DynamicMethod_Hack_Emit_Newobj() public static Func 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(); @@ -194,14 +194,14 @@ public static Func Get_DynamicMethod_Emit_Newobj() il.Emit(OpCodes.Newobj, _ctor); il.Emit(OpCodes.Ret); - return (Func)dynMethod.CreateDelegate(typeof(Func), ExpressionCompiler.EmptyArrayClosure); + return (Func)dynMethod.CreateDelegate(typeof(Func), ExpressionCompiler.EmptyClosure.Instance); } public static Func 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(); @@ -228,7 +228,7 @@ public static Func Get_DynamicMethod_Hack_Emit_Newobj() il.Emit(OpCodes.Ret); - return (Func)dynMethod.CreateDelegate(typeof(Func), ExpressionCompiler.EmptyArrayClosure); + return (Func)dynMethod.CreateDelegate(typeof(Func), ExpressionCompiler.EmptyClosure.Instance); } public class A diff --git a/test/FastExpressionCompiler.IssueTests/Issue461_InvalidProgramException_when_null_checking_type_by_ref.cs b/test/FastExpressionCompiler.IssueTests/Issue461_InvalidProgramException_when_null_checking_type_by_ref.cs index 790462e4..0d79ac0b 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue461_InvalidProgramException_when_null_checking_type_by_ref.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue461_InvalidProgramException_when_null_checking_type_by_ref.cs @@ -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 @@ -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>( + 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"); 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..31878660 --- /dev/null +++ b/test/FastExpressionCompiler.IssueTests/Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET.cs @@ -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> 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(); + // 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()); + + // 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 + } +} diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 9a2840bc..6e4d4547 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -9,9 +9,13 @@ 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 LightExpression.IssueTests.Issue363_ActionFunc16Generics().Run(); + + // 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(); // new LightExpression.IssueTests.Issue461_InvalidProgramException_when_null_checking_type_by_ref().Run(); diff --git a/test/FastExpressionCompiler.UnitTests/BinaryExpressionTests.cs b/test/FastExpressionCompiler.UnitTests/BinaryExpressionTests.cs index 37269b55..9d77cb02 100644 --- a/test/FastExpressionCompiler.UnitTests/BinaryExpressionTests.cs +++ b/test/FastExpressionCompiler.UnitTests/BinaryExpressionTests.cs @@ -1,6 +1,5 @@ using System; - #if LIGHT_EXPRESSION using ExpressionType = System.Linq.Expressions.ExpressionType; using static FastExpressionCompiler.LightExpression.Expression;