尽管这是一个相当古老的问题,但我发现它很有趣,因为有几种动态调用方法的选项。从字面上看,它是反射、表达式树和发射器。从历史上看,反射是最慢的选项,而发射器是最快的选项。因此,我决定在这个有趣的案例中比较它们,并找出现在是否有任何变化。原始问题询问**当编译时类型参数未知时调用泛型方法的最佳方式**。然而,几乎所有上述答案都建议使用反射。
我为所有提到的方法创建了三个测试用例。首先,这是一个略微修改过的示例类,将使用 3 种方法进行测试:TestReflection、TestExpression 和 TestEmit。
public class Sample
{
public void TestDirectCall(Type type)
{
GenericMethod<string>();
GenericMethodWithArg<string>(42);
StaticMethod<string>();
StaticMethodWithArg<string>(6);
}
public void TestReflection(Type type)
{
CallViaReflection.CallGenericMethod(this, type);
CallViaReflection.CallGenericMethod(this, type, 42);
CallViaReflection.CallStaticMethod(type);
CallViaReflection.CallStaticMethod(type, 6);
}
public void TestExpression(Type type)
{
CallViaExpression.CallGenericMethod(this, type);
CallViaExpression.CallGenericMethod(this, type, 42);
CallViaExpression.CallStaticMethod(type);
CallViaExpression.CallStaticMethod(type, 6);
}
public void TestEmit(Type type)
{
CallViaEmit.CallGenericMethod(this, type);
CallViaEmit.CallGenericMethod(this, type, 42);
CallViaEmit.CallStaticMethod(type);
CallViaEmit.CallStaticMethod(type, 6);
}
public void T()
{
StaticMethod<string>();
}
public void GenericMethod<T>()
{
}
public void GenericMethodWithArg<T>(int someValue)
{
}
public static void StaticMethod<T>()
{
}
public static void StaticMethodWithArg<T>(int someValue)
{
}
}
CallViaReflection 类表示通过反射调用通用方法的辅助类。我决定引入缓存以获得更好的结果。
public static class CallViaReflection
{
private readonly static Cache<MethodInfo> cache = new();
public static void CallGenericMethod(Sample sample, Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
callDelegate.Invoke(sample, null);
}
public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType, typeof(int));
callDelegate.Invoke(sample, new object[] { someValue });
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
callDelegate.Invoke(null, null);
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType, typeof(int));
callDelegate.Invoke(null, new object[] { someValue });
}
private static MethodInfo GetDelegate(string methodName, BindingFlags bindingFlags, Type genericType, params Type[] arguments)
{
if (cache.TryGet(methodName, genericType, out var concreteMethodInfo))
return concreteMethodInfo;
var sampleType = typeof(Sample);
MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
cache.Add(methodName, genericType, concreteMethodInfo);
return concreteMethodInfo;
}
}
下一个类 CallViaExpression 使用缓存的表达式树。
public static class CallViaExpression
{
private static readonly Cache<Delegate> cache = new();
public static void CallGenericMethod(Sample sample, Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
((Action<Sample>)callDelegate).Invoke(sample);
}
public static void CallGenericMethod(Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType, typeof(int));
((Action<Sample, int>)callDelegate).Invoke(sample, someValue);
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
((Action)callDelegate).Invoke();
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDelegate(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType, typeof(int));
((Action<int>)callDelegate).Invoke(someValue);
}
private static Delegate GetDelegate(string methodName, BindingFlags bindingFlags, Type genericType, params Type[] arguments)
{
if (cache.TryGet(methodName, genericType, out var callDelegate))
return callDelegate;
var sampleType = typeof(Sample);
MethodInfo genericMethodInfo = sampleType.GetMethod(methodName, bindingFlags)!;
var concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
var argumentExpr = arguments.Select((type, i) => Expression.Parameter(type, "arg" + i)).ToArray();
if (concreteMethodInfo.IsStatic)
{
var callExpr = Expression.Call(concreteMethodInfo, argumentExpr);
callDelegate = Expression.Lambda(callExpr, argumentExpr).Compile();
}
else
{
var parameterExpr = Expression.Parameter(sampleType, "sample");
var callExpr = Expression.Call(parameterExpr, concreteMethodInfo, argumentExpr);
callDelegate = Expression.Lambda(callExpr, new[] { parameterExpr }.Union(argumentExpr).ToArray()).Compile();
}
cache.Add(methodName, genericType, callDelegate);
return callDelegate;
}
}
最后但并非最不重要的一点是,CallViaEmit 发出了必要的操作。
public static class CallViaEmit
{
private static readonly Cache<Delegate> cache = new();
public static void CallGenericMethod(this Sample sample, Type genericType)
{
var callDelegate = GetDynamicMethod(nameof(Sample.GenericMethod), BindingFlags.Instance | BindingFlags.Public, genericType);
((Action<Sample>)callDelegate).Invoke(sample);
}
public static void CallGenericMethod(this Sample sample, Type genericType, int someValue)
{
var callDelegate = GetDynamicMethod(nameof(Sample.GenericMethodWithArg), BindingFlags.Instance | BindingFlags.Public, genericType);
((Action<Sample, int>)callDelegate).Invoke(sample, someValue);
}
public static void CallStaticMethod(Type genericType)
{
var callDelegate = GetDynamicMethod(nameof(Sample.StaticMethod), BindingFlags.Static | BindingFlags.Public, genericType);
((Action)callDelegate).Invoke();
}
public static void CallStaticMethod(Type genericType, int someValue)
{
var callDelegate = GetDynamicMethod(nameof(Sample.StaticMethodWithArg), BindingFlags.Static | BindingFlags.Public, genericType);
((Action<int>)callDelegate).Invoke(someValue);
}
private static Delegate GetDynamicMethod(string methodName, BindingFlags bindingFlags, Type genericType)
{
if (cache.TryGet(methodName, genericType, out var callDelegate))
return callDelegate;
var genericMethodInfo = typeof(Sample).GetMethod(methodName, bindingFlags)!;
var concreteMethodInfo = genericMethodInfo.MakeGenericMethod(genericType);
var argumentTypes = concreteMethodInfo.GetParameters().Select(x => x.ParameterType).ToArray(); ;
var dynamicMethodArgs = concreteMethodInfo.IsStatic
? argumentTypes
: new[] { typeof(Sample) }.Union(argumentTypes).ToArray();
var dynamicMethod = new DynamicMethod("DynamicCall", null, dynamicMethodArgs);
var il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Nop);
switch (dynamicMethodArgs.Length)
{
case 0:
break;
case 1:
il.Emit(OpCodes.Ldarg_0);
break;
case 2:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
break;
case 3:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
break;
default:
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Ldarg_3);
for (int i = 4; i < argumentTypes.Length; i++)
{
il.Emit(OpCodes.Ldarg, argumentTypes[i]);
}
break;
}
il.EmitCall(concreteMethodInfo.IsStatic ? OpCodes.Call : OpCodes.Callvirt, concreteMethodInfo, null);
il.Emit(OpCodes.Nop);
il.Emit(OpCodes.Ret);
callDelegate = dynamicMethod.CreateDelegate(GetActionType(dynamicMethodArgs));
cache.Add(methodName, genericType, callDelegate);
return callDelegate;
}
private static Type GetActionType(Type[] argumentTypes)
{
switch (argumentTypes.Length)
{
case 0:
return typeof(Action);
case 1:
return typeof(Action<>).MakeGenericType(argumentTypes);
case 2:
return typeof(Action<,>).MakeGenericType(argumentTypes);
case 3:
return typeof(Action<,,>).MakeGenericType(argumentTypes);
case 4:
return typeof(Action<,,,>).MakeGenericType(argumentTypes);
case 5:
return typeof(Action<,,,,>).MakeGenericType(argumentTypes);
case 6:
return typeof(Action<,,,,,>).MakeGenericType(argumentTypes);
case 7:
return typeof(Action<,,,,,,>).MakeGenericType(argumentTypes);
case 8:
return typeof(Action<,,,,,,,>).MakeGenericType(argumentTypes);
default:
throw new NotSupportedException("Action with more than 8 arguments is not supported");
}
}
}
最后,这里是一个测试类和测试结果。
[TestFixture]
public class SampleTests
{
private const int Iterations = 10000000;
[Test]
public void TestDirectCall()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestDirectCall(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods directly took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestReflection()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestReflection(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via reflection took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestExpressionTree()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestExpression(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via expression tree took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
[Test]
public void TestEmit()
{
var sample = new Sample();
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < Iterations; i++)
sample.TestEmit(typeof(string));
stopwatch.Stop();
Assert.Pass($"Calling methods dynamically via emit took {stopwatch.ElapsedMilliseconds} milliseconds.");
}
}
通过 emit 动态调用方法花费了 2939 毫秒。通过表达式树动态调用方法花费了 3910 毫秒。通过反射动态调用方法花费了 6381 毫秒。
显然,获胜者是发射器。它的性能仍然快两倍多。第二名是表达式树。
因此,我的结论在第二个十年内还没有改变。如果您需要动态调用方法,请开始使用表达式树。如果您的代码对性能至关重要,请使用 ILGenerator 并发出必要的调用。尽管乍一看可能很复杂,但所有必要的 MSIL 指令都可以使用 ildasm .
源代码可在 GitHub .