Tengo una pregunta avanzada y hasta ahora me ha llevado muchas horas intentar encontrar una solución que no utilice la reflexión.
El escenario es que tenemos una aplicación C++ que ejecuta nuestro propio lenguaje de secuencias de comandos interpretado y ese lenguaje de secuencias de comandos puede interactuar con C# (.NET), por ejemplo, para crear cuadros de diálogo de Windows Forms. Logramos esto mediante el uso de ICorRuntimeHost, una biblioteca auxiliar de C++/CLI y, en última instancia, reflexión para llamar a métodos, propiedades, etc.
He dado un paso atrás y estoy tratando de ver cómo puedo ejecutar el código de la manera más eficiente en C# sin tener que escribir las llamadas al método directamente. Estoy usando BenchmarkDotNet para comparar el código. También tengo la intención de usar solo C ++/CLI sin ICorRuntimeHost, lo que significaría que el código C # a continuación debería escribirse en C ++/CLI en última instancia.
El código de referencia es el siguiente:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; namespace DotNetExecutorTest { public class BenchmarksIndexOf { private readonly int NumberOfCalls = 1000; [Benchmark] public long Reflection() { long sum = 0; for (int i = 0; i < NumberOfCalls; i++) { sum += (int)(typeof(string).InvokeMember("IndexOf", System.Reflection.BindingFlags.InvokeMethod, null, "Hello", new object[] { 'H' })); } return sum; } [Benchmark] public long TypedOpenDelegate() { long sum = 0; var methodInfo = typeof(string).GetMethod("IndexOf", new Type[] { typeof(char) }); Func<string, char, int> func = (Func<string, char, int>)Delegate.CreateDelegate(typeof(Func<string, char, int>), methodInfo); for (int i = 0; i < NumberOfCalls; i++) { sum += func("Hello", 'H'); } return sum; } [Benchmark] public long NativeCSharp() { long sum = 0; for (int i = 0; i < NumberOfCalls; i++) { sum += "Hello".IndexOf('H'); } return sum; } [Benchmark] public long FastExecutorV1() { long sum = 0; for (int i = 0; i < NumberOfCalls; i++) { sum += (int)(DotNetExecutorFastV1.ExecuteMethod(typeof(string), "IndexOf", false, "Hello", new Type[] { typeof(char) }, new object[] { 'H' })); } return sum; } [Benchmark] public long FastExecutorV2() { long sum = 0; for (int i = 0; i < NumberOfCalls; i++) { sum += (int)(DotNetExecutorFastV2.ExecuteMethod(typeof(string), "IndexOf", false, "Hello", new Type[] { typeof(char) }, new object[] { 'H' })); } return sum; } [Benchmark] public long SlowExecutor() { long sum = 0; for (int i = 0; i < NumberOfCalls; i++) { sum += (int)(DotNetExecutorSlow.ExecuteMethod(typeof(string), "IndexOf", false, "Hello", new Type[] { typeof(char) }, new object[] { 'H' })); } return sum; } [Benchmark] public long MethodInfoExecutor() { long sum = 0; for (int i = 0; i < NumberOfCalls; i++) { sum += (int)(DotNetExecutorMethodInfo.ExecuteMethod(typeof(string), "IndexOf", false, "Hello", new Type[] { typeof(char) }, new object[] { 'H' })); } return sum; } } }Y los resultados son:
| Método | Significar | Error | Desv.estándar | Mediana |
|---|---|---|---|---|
| Reflexión | 1,106.853 us | 53.9937 nosotros | 159.2016 nosotros | 1,000.888 us |
| TypedOpenDelegate | 6.937 us | 0.0802 nosotros | 0.0626 nosotros | 6.942 us |
| NativeCSharp | 3.328 us | 0.0430 nosotros | 0.0381 nosotros | 3.312 nosotros |
| FastExecutorV1 | 208.947 nosotros | 1.1720 nosotros | 1.0390 nosotros | 208.974 nosotros |
| FastExecutorV2 | 203.321 nosotros | 3.9627 nosotros | 4.2400 us | 202.109 nosotros |
| Ejecutor lento | 1,184.866 us | 7.4664 nosotros | 6.2348 nosotros | 1,182.970 us |
| MethodInfoExecutor | 401.875 nosotros | 2.3886 nosotros | 2.1175 nosotros | 402.048 nosotros |
La explicación rápida de los métodos es la siguiente:
Func<string, char, int> y ahí parece ser exactamente donde se gana el rendimiento.Func<object, object[], object> , según delegados abiertos, se explicará más adelante.Func<object, object[], object> , en función de llamadas a métodos, se explicará más adelante.Entonces, primero puedo explicar el MethodInfoExecutor, en realidad es bastante simple:
public class DotNetExecutorMethodInfo { private static readonly Dictionary<DotNetKey, MethodInfo> methodInfos = new Dictionary<DotNetKey, MethodInfo>(); public static void ResetDictionary() { methodInfos.Clear(); } public static object ExecuteMethod(Type type, string name, bool isStatic, object instance, Type[] parameterTypes, object[] parameters) { var dotNetKey = new DotNetKey { type = type, dotNetType = DotNetType.Method, name = name, isStatic = isStatic, parameterTypes = parameterTypes }; if (!methodInfos.TryGetValue(dotNetKey, out var methodInfo)) { methodInfo = type.GetMethod(name, BindingFlags.Public | (isStatic ? BindingFlags.Static : BindingFlags.Instance), null, parameterTypes, null); methodInfos[dotNetKey] = methodInfo; } return methodInfo.Invoke(isStatic ? null : instance, parameters); } }Para completar la fuente DotNetKey:
public struct DotNetKey : IEquatable<DotNetKey> { public Type type; public DotNetType dotNetType; public bool isStatic; public string name; public Type[] parameterTypes; public bool Equals(DotNetKey other) { if (type != other.type) { return false; } if (dotNetType != other.dotNetType) { return false; } if (isStatic != other.isStatic) { return false; } if (name != other.name) { return false; } if (!Enumerable.SequenceEqual(parameterTypes, other.parameterTypes)) { return false; } return true; } public override int GetHashCode() { var hash = 17 * (type, dotNetType, isStatic, name).GetHashCode(); foreach (var type in parameterTypes) { hash = hash * 23 + type.GetHashCode(); } return hash; } }Y DotNetType:
public enum DotNetType { Method }Y luego tenemos nuestra solución compleja que es la siguiente, incluso tiene dos versiones con el mismo rendimiento.
Versión 1, la versión original con delegados abiertos:
public class DotNetExecutorFastV1 { private static readonly Dictionary<DotNetKey, Func<object, object[], object>> funcs = new Dictionary<DotNetKey, Func<object, object[], object>>(); public static object ExecuteMethod(Type type, string name, bool isStatic, object instance, Type[] parameterTypes, object[] parameters) { var func = FindFunc(type, DotNetType.Method, name, isStatic, parameterTypes); return func(instance, parameters); } private static Func<object, object[], object> FindFunc(Type type, DotNetType dotNetType, string name, bool isStatic, Type[] parameterTypes) { var dotNetKey = new DotNetKey { type = type, dotNetType = dotNetType, name = name, isStatic = isStatic, parameterTypes = parameterTypes }; if (funcs.TryGetValue(dotNetKey, out var func)) { return func; } else { var newFunc = CreateMethodFunc(type, name, isStatic, parameterTypes); funcs.Add(dotNetKey, newFunc); return newFunc; } } private static Func<object, object[], object> CreateMethodFunc(Type type, string name, bool isStatic, Type[] parameterTypes) { var methodInfo = type.GetMethod(name, BindingFlags.Public | (isStatic ? BindingFlags.Static : BindingFlags.Instance), null, parameterTypes, null); var openDelegate = Delegate.CreateDelegate(OpenDelegateType(type, isStatic, methodInfo, parameterTypes), methodInfo); var instance = Expression.Parameter(typeof(object), "instance"); var parameterArray = Expression.Parameter(typeof(object[]), "parameterArray"); var parameterExpressions = parameterTypes.Select((parameterType, index) => Expression.Convert(Expression.ArrayAccess(parameterArray, Expression.Constant(index)), parameterType)); var parameters = new List<Expression>(); if (isStatic) { parameters.AddRange(parameterExpressions); } else { parameters.Add(Expression.Convert(instance, isStatic ? typeof(object) : type)); parameters.AddRange(parameterExpressions); } var invokeExpression = Expression.Invoke( Expression.Constant(openDelegate), parameters ); Expression bodyExpression; if (methodInfo.ReturnType == typeof(void)) { bodyExpression = Expression.Block( invokeExpression, Expression.Constant(null) ); } else { bodyExpression = Expression.Convert(invokeExpression, typeof(object)); } var lambda = Expression.Lambda<Func<object, object[], object>>( bodyExpression, instance, parameterArray ); return lambda.Compile(); } private static Type OpenDelegateType(Type type, bool isStatic, MethodInfo methodInfo, Type[] parameterTypes) { var types = new List<Type>(); if (!isStatic) { types.Add(type); // type of instance class } types.AddRange(parameterTypes); // parameters if (methodInfo.ReturnType != typeof(void)) { types.Add(methodInfo.ReturnType); // return type } if (methodInfo.ReturnType == typeof(void)) { return Expression.GetActionType(types.ToArray()); } else { return Expression.GetFuncType(types.ToArray()); } } }Y la versión 2, que usa un CreateMethodFunc diferente:
public class DotNetExecutorFastV2 { private static readonly Dictionary<DotNetKey, Func<object, object[], object>> funcs = new Dictionary<DotNetKey, Func<object, object[], object>>(); public static object ExecuteMethod(Type type, string name, bool isStatic, object instance, Type[] parameterTypes, object[] parameters) { var func = FindFunc(type, DotNetType.Method, name, isStatic, parameterTypes); return func(instance, parameters); } private static Func<object, object[], object> FindFunc(Type type, DotNetType dotNetType, string name, bool isStatic, Type[] parameterTypes) { var dotNetKey = new DotNetKey { type = type, dotNetType = dotNetType, name = name, isStatic = isStatic, parameterTypes = parameterTypes }; if (funcs.TryGetValue(dotNetKey, out var func)) { return func; } else { var newFunc = CreateMethodFunc(type, name, isStatic, parameterTypes); funcs.Add(dotNetKey, newFunc); return newFunc; } } private static Func<object, object[], object> CreateMethodFunc(Type type, string name, bool isStatic, Type[] parameterTypes) { var methodInfo = type.GetMethod(name, BindingFlags.Public | (isStatic ? BindingFlags.Static : BindingFlags.Instance), null, parameterTypes, null); var instance = Expression.Parameter(typeof(object), "instance"); var parameterArray = Expression.Parameter(typeof(object[]), "parameterArray"); var parameterExpressions = parameterTypes.Select((parameterType, index) => Expression.Convert(Expression.ArrayAccess(parameterArray, Expression.Constant(index)), parameterType)); Expression callExpression; if (isStatic) { callExpression = Expression.Call( methodInfo, parameterExpressions ); } else { callExpression = Expression.Call( Expression.Convert(instance, type), methodInfo, parameterExpressions ); } Expression bodyExpression; if (methodInfo.ReturnType == typeof(void)) { bodyExpression = Expression.Block( callExpression, Expression.Constant(null) ); } else { bodyExpression = Expression.Convert(callExpression, typeof(object)); } var lambda = Expression.Lambda<Func<object, object[], object>>( bodyExpression, instance, parameterArray ); return lambda.Compile(); } } Básicamente, lo que estoy haciendo es intentar desbloquear el rendimiento de los delegados fuertemente tipados escribiéndolos como Func<object, object[], object> pero en algún lugar sigo perdiendo rendimiento, no estoy seguro de si eso sucede debido a la llamada de método adicional o por el acceso a la matriz o por otra cosa.
Así que la pregunta sigue siendo: ¿Cómo aceleraría esto? Idealmente, obtendría un rendimiento cercano al nativo y debe ser completamente dinámico, ya que el sitio de llamadas de C ++ debería poder invocar cualquier método C # (.NET).