diff --git a/src/Umbraco.Core/ExpressionExtensions.cs b/src/Umbraco.Core/ExpressionExtensions.cs index 392316e4eb..3fc602927b 100644 --- a/src/Umbraco.Core/ExpressionExtensions.cs +++ b/src/Umbraco.Core/ExpressionExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Linq.Expressions; namespace Umbraco.Core @@ -7,22 +6,64 @@ namespace Umbraco.Core internal static class ExpressionExtensions { public static Expression> True() { return f => true; } + public static Expression> False() { return f => false; } - public static Expression> Or(this Expression> left, - Expression> right) + public static Expression> Or(this Expression> left, Expression> right) { - var invokedExpr = Expression.Invoke(right, left.Parameters.Cast()); - return Expression.Lambda> - (Expression.OrElse(left.Body, invokedExpr), left.Parameters); + var invokedExpr = Expression.Invoke(right, left.Parameters); + return Expression.Lambda>(Expression.OrElse(left.Body, invokedExpr), left.Parameters); } - public static Expression> And(this Expression> left, - Expression> right) + public static Expression> And(this Expression> left, Expression> right) { - var invokedExpr = Expression.Invoke(right, left.Parameters.Cast()); - return Expression.Lambda> - (Expression.AndAlso(left.Body, invokedExpr), left.Parameters); + var invokedExpr = Expression.Invoke(right, left.Parameters); + return Expression.Lambda> (Expression.AndAlso(left.Body, invokedExpr), left.Parameters); + } + + public static Action CompileToDelegate(this Expression expr) + { + return ReflectionUtilities.CompileToDelegate(expr); + } + + public static Action CompileToDelegate(this Expression> expr) + { + return ReflectionUtilities.CompileToDelegate(expr); + } + + public static Action CompileToDelegate(this Expression> expr) + { + return ReflectionUtilities.CompileToDelegate(expr); + } + + public static Action CompileToDelegate(this Expression> expr) + { + return ReflectionUtilities.CompileToDelegate(expr); + } + + public static Func CompileToDelegate(this Expression> expr) + { + return ReflectionUtilities.CompileToDelegate(expr); + } + + public static Func CompileToDelegate(this Expression> expr) + { + return ReflectionUtilities.CompileToDelegate(expr); + } + + public static Func CompileToDelegate(this Expression> expr) + { + return ReflectionUtilities.CompileToDelegate(expr); + } + + public static Func CompileToDelegate(this Expression> expr) + { + return ReflectionUtilities.CompileToDelegate(expr); + } + + public static TMethod CompileToDelegate(this Expression expr) + { + return ReflectionUtilities.CompileToDelegate(expr); } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs index cc6ac093b4..898a575c02 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; @@ -39,6 +40,7 @@ namespace Umbraco.Core.Models.PublishedContent { var ctorArgTypes = new[] { typeof(IPublishedElement) }; var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var exprs = new List>>(); ModelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); @@ -68,31 +70,16 @@ namespace Umbraco.Core.Models.PublishedContent if (modelInfos.TryGetValue(typeName, out ModelInfo modelInfo)) throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\"."); - // see Umbraco.Tests.Benchmarks.CtorInvokeBenchmarks - // using ctor.Invoke is horrible, cannot even consider it, - // then expressions are 6-10x slower than direct ctor, and - // dynamic methods are 2-3x slower than direct ctor = best - - // much faster with a dynamic method but potential MediumTrust issues - which we don't support - // here http://stackoverflow.com/questions/16363838/how-do-you-call-a-constructor-via-an-expression-tree-on-an-existing-object - var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true); - var gen = meth.GetILGenerator(); - gen.Emit(OpCodes.Ldarg_0); - gen.Emit(OpCodes.Newobj, constructor); - gen.Emit(OpCodes.Ret); - var func = (Func) meth.CreateDelegate(typeof (Func)); - - // fast enough and works in MediumTrust - but we don't - // read http://boxbinary.com/2011/10/how-to-run-a-unit-test-in-medium-trust-with-nunitpart-three-umbraco-framework-testing/ - //var exprArg = Expression.Parameter(typeof(IPropertySet), "content"); - //var exprNew = Expression.New(constructor, exprArg); - //var expr = Expression.Lambda>(exprNew, exprArg); - //var func = expr.Compile(); - - modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type }; + exprs.Add(Expression.Lambda>(Expression.New(constructor))); + modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, ModelType = type }; ModelTypeMap[typeName] = type; } + var compiled = ReflectionUtilities.CompileToDelegates(exprs.ToArray()); + var i = 0; + foreach (var modelInfo in modelInfos.Values) + modelInfo.Ctor = compiled[i++]; + _modelInfos = modelInfos.Count > 0 ? modelInfos : null; } diff --git a/src/Umbraco.Core/ReflectionUtilities.cs b/src/Umbraco.Core/ReflectionUtilities.cs new file mode 100644 index 0000000000..372f30d1f5 --- /dev/null +++ b/src/Umbraco.Core/ReflectionUtilities.cs @@ -0,0 +1,572 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; + +namespace Umbraco.Core +{ + /// + /// Provides utilities to simplify reflection. + /// + public static class ReflectionUtilities + { + private static Func GetPropertyGetter(PropertyInfo property) + { + var type = typeof(TInstance); + + var getMethod = property.GetMethod; + if (getMethod == null) + throw new InvalidOperationException($"Property {type}.{property.Name} : {property.PropertyType} does not have a getter."); + + var exprThis = Expression.Parameter(type, "this"); + var exprCall = Expression.Call(exprThis, getMethod); + var expr = Expression.Lambda>(exprCall, exprThis); + return expr.CompileToDelegate(); + } + + private static Action GetPropertySetter(PropertyInfo property) + { + var type = typeof(TInstance); + + var setMethod = property.SetMethod; + if (setMethod == null) + throw new InvalidOperationException($"Property {type}.{property.Name} : {property.PropertyType} does not have a setter."); + + var exprThis = Expression.Parameter(type, "this"); + var exprArg0 = Expression.Parameter(typeof(TValue), "value"); + var exprCall = Expression.Call(exprThis, setMethod, exprArg0); + var expr = Expression.Lambda>(exprCall, exprThis, exprArg0); + return expr.CompileToDelegate(); + } + + public static Func GetPropertyGetter(string propertyName) + { + var type = typeof(TInstance); + var type0 = typeof(TValue); + var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (property == null || property.PropertyType != type0) + throw new InvalidOperationException($"Could not get property {type}.{propertyName} : {type0}."); + return GetPropertyGetter(property); + } + + public static Action GetPropertySetter(string propertyName) + { + var type = typeof(TInstance); + var type0 = typeof(TValue); + var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (property == null || property.PropertyType != type0) + throw new InvalidOperationException($"Could not get property {type}.{propertyName} : {type0}."); + return GetPropertySetter(property); + } + + public static void GetPropertyGetterSetter(string propertyName, out Func getter, out Action setter) + { + var type = typeof(TInstance); + var type0 = typeof(TValue); + var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (property == null || property.PropertyType != type0) + throw new InvalidOperationException($"Could not get property {type}.{propertyName} : {type0}."); + getter = GetPropertyGetter(property); + setter = GetPropertySetter(property); + } + + public static Func GetCtor() + { + var type = typeof(TInstance); + + // get the constructor infos + var ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, + null, Type.EmptyTypes, null); + + if (ctor == null) + throw new InvalidOperationException($"Could not find constructor {type}.ctor()."); + + var exprNew = Expression.New(ctor); + var expr = Expression.Lambda>(exprNew); + return expr.CompileToDelegate(); + } + + public static Func GetCtor() + { + var type = typeof(TInstance); + var type0 = typeof(TArg0); + + // get the constructor infos + var ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, + null, new[] { type0 }, null); + + if (ctor == null) + throw new InvalidOperationException($"Could not find constructor {type}.ctor({type0})."); + + var exprArgs = ctor.GetParameters().Select(x => Expression.Parameter(x.ParameterType, x.Name)).ToArray(); + // ReSharper disable once CoVariantArrayConversion + var exprNew = Expression.New(ctor, exprArgs); + var expr = Expression.Lambda>(exprNew, exprArgs); + return expr.CompileToDelegate(); + } + + public static Func GetCtor() + { + var type = typeof(TInstance); + var type0 = typeof(TArg0); + var type1 = typeof(TArg1); + + // get the constructor infos + var ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, + null, new[] { type0, type1 }, null); + + if (ctor == null) + throw new InvalidOperationException($"Could not find constructor {type}.ctor({type0}, {type1})."); + + var exprArgs = ctor.GetParameters().Select(x => Expression.Parameter(x.ParameterType, x.Name)).ToArray(); + // ReSharper disable once CoVariantArrayConversion + var exprNew = Expression.New(ctor, exprArgs); + var expr = Expression.Lambda>(exprNew, exprArgs); + return expr.CompileToDelegate(); + } + + public static TMethod GetMethod(MethodInfo method) + { + var type = method.DeclaringType; + + GetMethodParms(out var parameterTypes, out var returnType); + return GetStaticMethod(method, method.Name, type, parameterTypes, returnType); + } + + public static TMethod GetMethod(MethodInfo method) + { + var type = method.DeclaringType; + + GetMethodParms(out var parameterTypes, out var returnType); + return GetMethod(method, method.Name, type, parameterTypes, returnType); + } + + public static TMethod GetMethod(string methodName) + { + var type = typeof(TInstance); + + GetMethodParms(out var parameterTypes, out var returnType); + + var method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, + null, parameterTypes, null); + + return GetMethod(method, methodName, type, parameterTypes, returnType); + } + + private static void GetMethodParms(out Type[] parameterTypes, out Type returnType) + { + var typeM = typeof(TMethod); + var typeList = new List(); + returnType = typeof(void); + + if (!typeof(MulticastDelegate).IsAssignableFrom(typeM) || typeM == typeof(MulticastDelegate)) + throw new InvalidOperationException("Invalid TMethod, must be a Func or Action."); + + var typeName = typeM.FullName; + if (typeName.StartsWith("System.Func`")) + { + var i = 0; + while (i < typeM.GenericTypeArguments.Length - 1) + typeList.Add(typeM.GenericTypeArguments[i++]); + returnType = typeM.GenericTypeArguments[i]; + } + else if (typeName.StartsWith("System.Action`")) + { + var i = 0; + while (i < typeM.GenericTypeArguments.Length) + typeList.Add(typeM.GenericTypeArguments[i++]); + } + else if (typeName == "System.Action") + { + // no args + } + else + throw new InvalidOperationException(typeName); + + parameterTypes = typeList.ToArray(); + } + + private static void GetMethodParms(out Type[] parameterTypes, out Type returnType) + { + var type = typeof(TInstance); + + var typeM = typeof(TMethod); + var typeList = new List(); + returnType = typeof(void); + + if (!typeof(MulticastDelegate).IsAssignableFrom(typeM) || typeM == typeof(MulticastDelegate)) + throw new InvalidOperationException("Invalid TMethod, must be a Func or Action."); + + var typeName = typeM.FullName; + if (!typeM.IsGenericType) + throw new InvalidOperationException(typeName); + if (typeM.GenericTypeArguments[0] != type) + throw new InvalidOperationException("Invalid TMethod, the first generic argument must be TInstance."); + if (typeName.StartsWith("System.Func`")) + { + var i = 1; + while (i < typeM.GenericTypeArguments.Length - 1) + typeList.Add(typeM.GenericTypeArguments[i++]); + returnType = typeM.GenericTypeArguments[i]; + } + else if (typeName.StartsWith("System.Action`")) + { + var i = 1; + while (i < typeM.GenericTypeArguments.Length) + typeList.Add(typeM.GenericTypeArguments[i++]); + } + else + throw new InvalidOperationException(typeName); + + parameterTypes = typeList.ToArray(); + } + + private static TMethod GetStaticMethod(MethodInfo method, string methodName, Type type, Type[] parameterTypes, Type returnType) + { + if (method == null || method.ReturnType != returnType) + throw new InvalidOperationException($"Could not find static method {type}.{methodName}({string.Join(",", parameterTypes.Select(x => x.ToString()))}) : {returnType}"); + + var e = new List(); + foreach (var p in method.GetParameters()) + e.Add(Expression.Parameter(p.ParameterType, p.Name)); + var exprCallArgs = e.ToArray(); + var exprLambdaArgs = exprCallArgs; + + // ReSharper disable once CoVariantArrayConversion + var exprCall = Expression.Call(method, exprCallArgs); + var expr = Expression.Lambda(exprCall, exprLambdaArgs); + return expr.CompileToDelegate(); + } + + private static TMethod GetMethod(MethodInfo method, string methodName, Type type, Type[] parameterTypes, Type returnType) + { + if (method == null || method.ReturnType != returnType) + throw new InvalidOperationException($"Could not find method {type}.{methodName}({string.Join(",", parameterTypes.Select(x => x.ToString()))}) : {returnType}"); + + var e = new List(); + foreach (var p in method.GetParameters()) + e.Add(Expression.Parameter(p.ParameterType, p.Name)); + var exprCallArgs = e.ToArray(); + + var exprThis = Expression.Parameter(type, "this"); + e.Insert(0, exprThis); + var exprLambdaArgs = e.ToArray(); + + // ReSharper disable once CoVariantArrayConversion + var exprCall = Expression.Call(exprThis, method, exprCallArgs); + var expr = Expression.Lambda(exprCall, exprLambdaArgs); + return expr.Compile(); + } + + public static object GetStaticProperty(this Type type, string propertyName, Func, PropertyInfo> filter = null) + { + var propertyInfo = GetPropertyInfo(type, propertyName, filter); + if (propertyInfo == null) + throw new ArgumentOutOfRangeException(nameof(propertyName), + $"Couldn't find property {propertyName} in type {type.FullName}"); + return propertyInfo.GetValue(null, null); + } + + public static object CallStaticMethod(this Type type, string methodName, params object[] parameters) + { + var methodInfo = GetMethodInfo(type, methodName); + if (methodInfo == null) + throw new ArgumentOutOfRangeException(nameof(methodName), + $"Couldn't find method {methodName} in type {type.FullName}"); + return methodInfo.Invoke(null, parameters); + } + + public static object CallMethod(this object obj, string methodName, params object[] parameters) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + Type type = obj.GetType(); + var methodInfo = GetMethodInfo(type, methodName); + if (methodInfo == null) + throw new ArgumentOutOfRangeException(nameof(methodName), + $"Couldn't find method {methodName} in type {type.FullName}"); + return methodInfo.Invoke(obj, parameters); + } + + public static object CallMethod(this object obj, string methodName, Func, MethodInfo> filter = null, params object[] parameters) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + Type type = obj.GetType(); + var methodInfo = GetMethodInfo(type, methodName, filter); + if (methodInfo == null) + throw new ArgumentOutOfRangeException(nameof(methodName), + $"Couldn't find method {methodName} in type {type.FullName}"); + return methodInfo.Invoke(obj, parameters); + } + + private static MethodInfo GetMethodInfo(Type type, string methodName, Func, MethodInfo> filter = null) + { + MethodInfo methodInfo; + do + { + try + { + methodInfo = type.GetMethod(methodName, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + } + catch (AmbiguousMatchException) + { + if (filter == null) throw; + + methodInfo = filter( + type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) + .Where(x => x.Name == methodName)); + } + type = type.BaseType; + } + while (methodInfo == null && type != null); + return methodInfo; + } + + private static PropertyInfo GetPropertyInfo(Type type, string propertyName, Func, PropertyInfo> filter = null) + { + PropertyInfo propInfo; + do + { + try + { + propInfo = type.GetProperty(propertyName, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + } + catch (AmbiguousMatchException) + { + if (filter == null) throw; + + propInfo = filter(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) + .Where(x => x.Name == propertyName)); + } + type = type.BaseType; + } + while (propInfo == null && type != null); + return propInfo; + } + + public static object GetPropertyValue(this object obj, string propertyName) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + Type objType = obj.GetType(); + PropertyInfo propInfo = GetPropertyInfo(objType, propertyName); + if (propInfo == null) + throw new ArgumentOutOfRangeException(nameof(propertyName), + $"Couldn't find property {propertyName} in type {objType.FullName}"); + return propInfo.GetValue(obj, null); + } + + public static object GetPropertyValue(this object obj, string propertyName, IDictionary propCache) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + Type objType = obj.GetType(); + PropertyInfo propInfo; + if (propCache.ContainsKey(propertyName)) + { + propInfo = propCache[propertyName]; + } + else + { + propInfo = GetPropertyInfo(objType, propertyName); + if (propInfo == null) + throw new ArgumentOutOfRangeException(nameof(propertyName), + $"Couldn't find property {propertyName} in type {objType.FullName}"); + + propCache[propertyName] = propInfo; + } + return propInfo.GetValue(obj, null); + } + + public static void SetPropertyValue(this object obj, string propertyName, object val) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + Type objType = obj.GetType(); + PropertyInfo propInfo = GetPropertyInfo(objType, propertyName); + if (propInfo == null) + throw new ArgumentOutOfRangeException(nameof(propertyName), + $"Couldn't find property {propertyName} in type {objType.FullName}"); + propInfo.SetValue(obj, val, null); + } + + public static void SetPropertyValue(this object obj, string propertyName, object val, IDictionary propCache) + { + if (obj == null) throw new ArgumentNullException(nameof(obj)); + if (propCache == null) throw new ArgumentNullException(nameof(propCache)); + + Type objType = obj.GetType(); + PropertyInfo propInfo; + if (propCache.ContainsKey(propertyName)) + { + propInfo = propCache[propertyName]; + } + else + { + propInfo = GetPropertyInfo(objType, propertyName); + if (propInfo == null) + throw new ArgumentOutOfRangeException(nameof(propertyName), + $"Couldn't find property {propertyName} in type {objType.FullName}"); + + propCache[propertyName] = propInfo; + } + + propInfo.SetValue(obj, val, null); + } + + public static Action CompileToDelegate(Expression expr) + { + var typeBuilder = CreateTypeBuilder(); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static); // CompileToMethod requires a static method + + expr.CompileToMethod(builder); + + return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); + } + + public static Action CompileToDelegate(Expression> expr) + { + var typeBuilder = CreateTypeBuilder(); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (void), new[] { typeof (T1) }); + + expr.CompileToMethod(builder); + + return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); + } + + public static Action CompileToDelegate(Expression> expr) + { + var typeBuilder = CreateTypeBuilder(); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (void), new[] { typeof(T1), typeof (T2) }); + + expr.CompileToMethod(builder); + + return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); + } + + public static Action CompileToDelegate(Expression> expr) + { + var typeBuilder = CreateTypeBuilder(); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (void), new[] { typeof (T1), typeof (T2) , typeof (T3) }); + + expr.CompileToMethod(builder); + + return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); + } + + public static Func CompileToDelegate(Expression> expr) + { + var typeBuilder = CreateTypeBuilder(); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, + typeof (TResult), Array.Empty()); // CompileToMethod requires a static method + + expr.CompileToMethod(builder); + + return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); + } + + public static Func CompileToDelegate(Expression> expr) + { + var typeBuilder = CreateTypeBuilder(); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (TResult), new[] { typeof (T1) }); + + expr.CompileToMethod(builder); + + return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); + } + + public static Func CompileToDelegate(Expression> expr) + { + var typeBuilder = CreateTypeBuilder(); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof(TResult), new[] { typeof (T1), typeof(T2) }); + + expr.CompileToMethod(builder); + + return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); + } + + public static Func CompileToDelegate(Expression> expr) + { + var typeBuilder = CreateTypeBuilder(); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (TResult), new[] { typeof (T1), typeof (T2), typeof (T3) }); + + expr.CompileToMethod(builder); + + return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); + } + + public static TMethod CompileToDelegate(Expression expr) + { + var typeBuilder = CreateTypeBuilder(); + + GetMethodParms(out var parameterTypes, out var returnType); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + returnType, parameterTypes); + + expr.CompileToMethod(builder); + + return (TMethod) (object) Delegate.CreateDelegate(typeof (TMethod), typeBuilder.CreateType().GetMethod("Method")); + } + + public static TMethod[] CompileToDelegates(params Expression[] exprs) + { + var typeBuilder = CreateTypeBuilder(); + + GetMethodParms(out var parameterTypes, out var returnType); + + var i = 0; + foreach (var expr in exprs) + { + var builder = typeBuilder.DefineMethod($"Method_{i++}", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + returnType, parameterTypes); + + expr.CompileToMethod(builder); + } + + var type = typeBuilder.CreateType(); + + var methods = new TMethod[exprs.Length]; + for (i = 0; i < exprs.Length; i++) + methods[i] = (TMethod) (object) Delegate.CreateDelegate(typeof (TMethod), type.GetMethod($"Method_{i++}")); + return methods; + } + + private static TypeBuilder CreateTypeBuilder() + { + var assemblyName = new AssemblyName("Umbraco.Core.DynamicAssemblies." + Guid.NewGuid().ToString("N")); + var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save); + var module = assembly.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll"); + return module.DefineType("Class", TypeAttributes.Public | TypeAttributes.Abstract); + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ae251bea45..4448bcfb4c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -28,6 +28,7 @@ 4 bin\Release\Umbraco.Core.xml false + latest @@ -1280,6 +1281,7 @@ + diff --git a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs index 7cce05c51d..46fe21f2c9 100644 --- a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs @@ -1,16 +1,13 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; -using System.Text; -using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Horology; using BenchmarkDotNet.Jobs; +using Umbraco.Core; namespace Umbraco.Tests.Benchmarks { @@ -28,16 +25,19 @@ namespace Umbraco.Tests.Benchmarks // see benchmarkdotnet FAQ Add(Job.Default .WithLaunchCount(1) // benchmark process will be launched only once - .WithIterationTime(TimeInterval.FromMilliseconds(100)) // 100ms per iteration - .WithWarmupCount(3) // 3 warmup iteration - .WithTargetCount(3)); // 3 target iteration + .WithIterationTime(TimeInterval.FromMilliseconds(400)) + .WithWarmupCount(3) + .WithTargetCount(6)); } } + private readonly IFoo _foo = new Foo(null); private ConstructorInfo _ctorInfo; private Func _dynamicMethod; - private Func _expression; - private IFoo _foo = new Foo(null); + private Func _expressionMethod; + private Func _expressionMethod2; + private Func _expressionMethod3; + private Func _expressionMethod4; [Setup] public void Setup() @@ -55,18 +55,91 @@ namespace Umbraco.Tests.Benchmarks //IL_000b: pop //IL_000c: ret - var meth = new DynamicMethod(string.Empty, typeof(Foo), ctorArgTypes, type.Module, true); + // generate a dynamic method + // + // ldarg.0 // obj0 + // newobj instance void [Umbraco.Tests.Benchmarks]Umbraco.Tests.Benchmarks.CtorInvokeBenchmarks / Foo::.ctor(class [Umbraco.Tests.Benchmarks] Umbraco.Tests.Benchmarks.CtorInvokeBenchmarks/IFoo) + // ret + + var meth = new DynamicMethod(string.Empty, typeof (IFoo), ctorArgTypes, type.Module, true); var gen = meth.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); - //gen.Emit(OpCodes.Call, constructor); gen.Emit(OpCodes.Newobj, constructor); gen.Emit(OpCodes.Ret); - _dynamicMethod = (Func) meth.CreateDelegate(typeof(Func)); + _dynamicMethod = (Func) meth.CreateDelegate(typeof (Func)); - var exprArg = Expression.Parameter(typeof(IFoo), "content"); + // generate a compiled expression + // + // ldarg.0 // content + // newobj instance void [Umbraco.Tests.Benchmarks]Umbraco.Tests.Benchmarks.CtorInvokeBenchmarks / Foo::.ctor(class [Umbraco.Tests.Benchmarks] Umbraco.Tests.Benchmarks.CtorInvokeBenchmarks/IFoo) + // ret + + var exprArg = Expression.Parameter(typeof (IFoo), "content"); var exprNew = Expression.New(constructor, exprArg); var expr = Expression.Lambda>(exprNew, exprArg); - _expression = expr.Compile(); + _expressionMethod = expr.Compile(); + + // create a dynamic assembly + // dump to disk so we can review IL code with eg DotPeek + + var assemblyName = new AssemblyName("Umbraco.Tests.Benchmarks.IL"); + var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save); + var module = assembly.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll"); + var typeBuilder = module.DefineType("CtorInvoke", TypeAttributes.Public | TypeAttributes.Abstract); + + var expressionMethodBuilder = typeBuilder.DefineMethod("ExpressionCtor", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (IFoo), ctorArgTypes); + expr.CompileToMethod(expressionMethodBuilder); + + var dynamicMethodBuilder = typeBuilder.DefineMethod("DynamicCtor", + MethodAttributes.Public | MethodAttributes.Static, + typeof(IFoo), ctorArgTypes); + gen = dynamicMethodBuilder.GetILGenerator(); + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Newobj, constructor); + gen.Emit(OpCodes.Ret); + meth.CreateDelegate(typeof (Func)); + + var btype = typeBuilder.CreateType(); // need to build before saving + assembly.Save("Umbraco.Tests.Benchmarks.IL.dll"); + + // at that point, + // _dynamicMethod is 2x slower than direct ctor + // _expressionMethod is 6x slower than direct ctor + // which is weird as inspecting the assembly IL shows that they are generated + // exactly the same, and yet it is confirmed eg by https://stackoverflow.com/questions/4211418 + // + // not sure why exactly + // see https://stackoverflow.com/questions/13431573 + // see http://mattwarren.org/2017/01/25/How-do-.NET-delegates-work/#different-types-of-delegates + // + // note that all the benchmark methods have the very same IL code so it's + // really the 'callvirt ...' that ends up doing different things + // + // more readings: + // http://byterot.blogspot.dk/2012/05/performance-comparison-of-code.html + // https://stackoverflow.com/questions/1296683 + // https://stackoverflow.com/questions/44239127 + // that last one points to + // https://blogs.msdn.microsoft.com/seteplia/2017/02/01/dissecting-the-new-constraint-in-c-a-perfect-example-of-a-leaky-abstraction/ + // which reads ... "Expression.Compile creates a DynamicMethod and associates it with an anonymous assembly + // to run it in a sandboxed environment. This makes it safe for a dynamic method to be emitted and executed + // by partially trusted code but adds some run-time overhead." + // and, turning things into a delegate (below) removes that overhead... + + // turning it into a delegate seems cool, _expressionMethod2 is ~ _dynamicMethod + _expressionMethod2 = (Func) Delegate.CreateDelegate(typeof (Func), btype.GetMethod("ExpressionCtor")); + + // nope, this won't work, throws an ArgumentException because 'MethodInfo must be a MethodInfo object' + // and here it's of type System.Reflection.Emit.DynamicMethod+RTDynamicMethod - whereas the btype one is ok + // so, the dynamic assembly step is required + // + //_expressionMethod3 = (Func) Delegate.CreateDelegate(typeof (Func), _expressionMethod.Method); + + // but, our utilities know how to do it! + _expressionMethod3 = expr.CompileToDelegate(); + _expressionMethod4 = ReflectionUtilities.GetCtor(); } public IFoo IlCtor(IFoo foo) @@ -74,7 +147,7 @@ namespace Umbraco.Tests.Benchmarks return new Foo(foo); } - [Benchmark] + [Benchmark(Baseline = true)] public void DirectCtor() { var foo = new Foo(_foo); @@ -95,7 +168,25 @@ namespace Umbraco.Tests.Benchmarks [Benchmark] public void ExpressionCtor() { - var foo = _expression(_foo); + var foo = _expressionMethod(_foo); + } + + [Benchmark] + public void Expression2Ctor() + { + var foo = _expressionMethod2(_foo); + } + + [Benchmark] + public void Expression3Ctor() + { + var foo = _expressionMethod3(_foo); + } + + [Benchmark] + public void Expression4Ctor() + { + var foo = _expressionMethod4(_foo); } public interface IFoo