通过 DynamicMethod 委托实现快速后期绑定调用






4.81/5 (23投票s)
本文介绍了一种通过在运行时生成自定义 MSIL 代码来实现快速后期绑定调用的方法。
引言
反射是一项允许程序在运行时了解对象类型(和元数据)信息的功能。支持反射的语言(如 Java 和 CLR 语言家族(C#、Visual Basic .NET 等))编写的程序可以检查类型、获取有关类成员的详细信息、动态实例化类并在运行时调用方法。 .NET Framework 通过 System.Reflection
命名空间公开其反射服务。例如,可以通过 Type.InvokeMember
和 MemberInfo.Invoke
方法实现后期绑定调用。然而,使用反射是有代价的。虽然它的一些功能相当快,但另一些功能(如后期绑定调用例程)成本很高,如果不谨慎使用,可能会成为应用程序中的主要瓶颈。
Joel Pobar 在其文章 规避常见的性能陷阱,打造快速应用程序 中,对 .NET 反射性能进行了很好的概述,并描述了一种实现快速后期绑定调用的方法,本文将讨论其实现。
通过 DynamicMethod 委托实现混合后期绑定到早期绑定的调用
Joel Polbar 提出的基本思想是通过通用的反射服务获取方法句柄,然后发出 MSIL 代码来修补方法的静态调用点。这个调用点将尽可能简单:不会执行类型安全/安全检查,假设混合调用仅在完全受信任的代码实体之间进行。MSIL 调用点的生成将通过 .NET Framework 2.0 的一项新功能——轻量级代码生成 (LGC) 来完成。在 .NET Framework 1.1 下也可以通过标准的 Reflection.Emit
功能实现混合调用,但实现会更复杂。
DynamicMethod 类
轻量级代码生成功能由 System.Reflection.Emit
命名空间中的一个新类公开:DynamicMethod
。您可以使用此类在运行时生成和执行方法,而无需生成动态程序集和包含该方法的动态类型。动态方法是生成和执行少量代码的最有效方式。
public DynamicMethod(
string name,
Type returnType,
Type[] parameterTypes,
Type owner
);
public DynamicMethod(
stringname,
TypereturnType,
Type[] parameterTypes,
Module owner
);
该类有两个公共构造函数。如您所见,动态方法在逻辑上与模块或类型(owner
参数)相关联。
代码概述
完整的实现位于一个 static
方法中
public static DynamicMethodDelegate DynamicMethodDelegateFactory.Create(
MethodInfo method
);
此函数接受一个 MethodInfo
,表示我们要调用的方法,并返回一个 DynamicMethodDelegate
委托
public delegate object DynamicMethodDelegate(
object instance,
object[] args
);
委托的语法与 MethodInfo.Invoke
重载之一相同:它接受一个 object
实例(对于静态方法,它可以为 null)和一个参数数组,并返回一个通用的 object
实例。
使用代码非常简单
MyClass instance = new MyClass();
MethodInfo myMethodInfo;
// [...] myMethodInfo = some method of MyClass
DynamicMethodDelegate myDelegate;
// Generate delegate for myMethodInfo
myDelegate = DynamicMethodDelegateFactory.Create(myMethodInfo);
// Invoke method through delegate.
object[] args = {/* method arguments here */};
object result = myDelegate(instance, args);
实现细节
我们方法的前半部分相当自明
/// <summary>
/// Generates a DynamicMethodDelegate delegate from a MethodInfo object.
/// </summary>
public static DynamicMethodDelegate Create(MethodInfo method)
{
ParameterInfo[] parms = method.GetParameters();
int numparams = parms.Length;
Type[] _argTypes = { typeof(object), typeof(object[]) };
// Create dynamic method and obtain its IL generator to
// inject code.
DynamicMethod dynam =
new DynamicMethod(
"",
typeof(object),
_argTypes,
typeof(DynamicMethodDelegateFactory));
ILGenerator il = dynam.GetILGenerator();
/* [...IL GENERATION...] */
return (DynamicMethodDelegate)
dynam.CreateDelegate(typeof(DynamicMethodDelegate));
}
现在让我们将注意力集中在 IL 生成代码上。我们的 IL 调用点将分为四个部分
- 参数计数检查
- 对象实例推送
- 参数布局
- 方法调用
参数计数检查
// Define a label for succesfull argument count checking.
Label argsOK = il.DefineLabel();
// Check input argument count.
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldlen);
il.Emit(OpCodes.Ldc_I4, numparams);
il.Emit(OpCodes.Beq, argsOK);
// Argument count was wrong, throw TargetParameterCountException.
il.Emit(OpCodes.Newobj,
typeof(TargetParameterCountException).GetConstructor(Type.EmptyTypes));
il.Emit(OpCodes.Throw);
// Mark IL with argsOK label.
il.MarkLabel(argsOK);
在此部分,我们比较提供的输入参数数组的长度与方法接受的参数数量。如果相等,我们将继续下一部分(用 argsOK
标签标记),否则将抛出 TargetParameterCountException
。
对象实例推送
// If method isn't static push target instance on top
// of stack.
if (!method.IsStatic)
{
// Argument 0 of dynamic method is target instance.
il.Emit(OpCodes.Ldarg_0);
}
清晰明了。函数调用 MSIL 指令(call
、calli
、callvirt
)需要在参数之前将目标对象引用推送到堆栈上。这显然只适用于非静态方法调用。
参数布局
// Lay out args array onto stack.
int i = 0;
while (i < numparams)
{
// Push args array reference onto the stack, followed
// by the current argument index (i). The Ldelem_Ref opcode
// will resolve them to args[i].
// Argument 1 of dynamic method is argument array.
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldc_I4, i);
il.Emit(OpCodes.Ldelem_Ref);
// If parameter [i] is a value type perform an unboxing.
Type parmType = parms[i].ParameterType;
if (parmType.IsValueType)
{
il.Emit(OpCodes.Unbox_Any, parmType);
}
i++;
}
我们读取 args
数组中的每个元素并将其推送到堆栈上。对于值类型参数,会执行解封(因为 args
数组是引用的集合,我们必须将值引用转换为值本身)。
方法调用
// Perform actual call.
// If method is not final a callvirt is required
// otherwise a normal call will be emitted.
if (method.IsFinal)
{
il.Emit(OpCodes.Call, method);
}
else
{
il.Emit(OpCodes.Callvirt, method);
}
if (method.ReturnType != typeof(void))
{
// If result is of value type it needs to be boxed
if (method.ReturnType.IsValueType)
{
il.Emit(OpCodes.Box, method.ReturnType);
}
}
else
{
il.Emit(OpCodes.Ldnull);
}
// Emit return opcode.
il.Emit(OpCodes.Ret);
在这里,我们生成实际的方法调用,处理可能的返回值,然后从调用点返回。如果我们的方法是最终方法(不是虚拟方法也不是接口实现方法),我们发出一个轻量级的 call
,否则需要 callvirt
。如果方法有返回值并且是值类型,则需要将其装箱,因为我们的 DynamicMethod
委托返回一个通用的 object
引用。如果方法返回类型为 void
,我们只需让我们的委托返回一个 null
值,并将其推送到堆栈上。
性能比较
如您所见,DynamicMethod
委托的实现已尽可能简化。与标准早期绑定调用相比,主要的开销在于需要装箱值类型返回值。这是一个代价高昂的操作,因为它涉及堆分配和被装箱值的副本。在创建输入参数数组时也可能发生装箱开销,但这在性能测量中并未考虑,因为它属于用户代码。
我测量了 DynamicMethod
委托与标准后期绑定调用在三种不同的调用场景下的效率,结果如下(**注意**:1 是同一调用场景下标准直接调用的效率)
== Testing performance of dynamic method delegates:
== Test call type: static method with boxing on return value
Results for 5 tests on 500000 iterations:
Direct method call: 6ms
Dynamic method delegates: 39ms (Efficiency: 0,15)
MethodInfo.Invoke: 4616ms (Efficiency: 0,001)
== Testing performance of dynamic method delegates:
== Test call type: static method without boxing on return value
Results for 5 tests on 100000 iterations:
Direct method call: 8ms
Dynamic method delegates: 14ms (Efficiency: 0,57)
MethodInfo.Invoke: 894ms (Efficiency: 0,009)
== Testing performance of dynamic method delegates
== Test call type: virtual method without boxing on return value
Results for 5 tests on 10000 iterations:
Direct method call: 10ms
Dynamic method delegates: 13ms (Efficiency: 0,77)
MethodInfo.Invoke: 166ms (Efficiency: 0,06)
在所有提出的调用场景中,DynamicMethod
委托的性能(在时间效率方面)都优于其他方法。但是,当发生返回值装箱时,如预期,后期绑定调用和混合调用都会遭受严重的效率损失。
结论
我试图说明 Joel Polbar 文章中暴露的一个想法的简单实现,并附带一些关于实现本身的观察,希望这项工作至少在一定程度上能帮助理解什么是混合调用以及如何实现它。最后,我想强调的是,这项技术**不能**替代通过反射进行的标准后期绑定调用。只有当一个在设计时未知的(或属性访问器)方法需要以中高频率在一个完全受信任的安全场景中调用时,才应该使用 DynamicMethod
委托代替 MethodInfo.Invoke
或类似的解决方案。
历史
- 2005 年 7 月 10 日 -> 首次发布。