动态对象、工厂和运行时机器以提高性能






4.79/5 (9投票s)
对动态对象实例化和运行时机器以提高性能的考察。
引言
有许多方法可以在运行时动态地实例化对象。 .NET Framework 内置了一些方法,例如 System.Activator
类、反射,以及一些你可以自己开发的方法。每种方法都有其自身的后果和收益。在我寻找动态实例化对象的最快方法的过程中,我汇总了有关各种方法、它们的实用性和各自性能的信息。请注意,本文适用于 .NET 1.1 - 我对 2.0 的了解仍然有限,因为我的主要工作环境是(更像是“正在努力成为”)一个 .NET 1.1 店。在更新的 .NET 版本中,还有其他方法可以实现本文中介绍的功能。
本文假定您对反射以及 MSIL 有一定的了解。我并非 MSIL 方面的专家,因此我不会在这方面花费太多时间。我需要进行大量实验,但幸运的是,有一些出色的工具可以使实验更容易。有关详细信息,请参阅“参考文献”部分。
背景
本文的内容并非新颖。这些性能基准测试已经进行了许多次。“参考文献”部分列出了我阅读并用于编写本文的文章。我的目的是提供基于我理解的比较,并演示如何创建一个工厂来使动态对象的创建尽可能快。我期待您的评论和建议。
创建动态对象的常用方法
实例化对象的最快方法是直接调用其构造函数。
Dim MyWidget As New SuperWidget
然而,这不允许我们编写通用的、与类型无关的代码。很多时候,一个 Type
直到运行时才能知道,所以让我们研究以下动态对象创建方法及其各自的性能。上面的截图显示了我的基准测试结果。如果您已经熟悉以下内容,您可能想跳到 动态工厂 部分。
System.Activator
-Activator.CreateInstance(type As System.Type)
- 反射 -
ConstructorInfo.Invoke()
- 反射 -
MethodInfo
和Delegate
System.Runtime.Serialization
-FormatterServices.GetUninitializedObject(type As System.Type)
System.Reflection.Emit
- 发射动态程序集来创建对象实例!
我将在下面的代码示例中使用以下 Type
Dim WidgetType As Type = Type.GetType("SuperWidget")
并且,我们所有的 Widget
都继承自
Public MustInherit Class Widget
Public MustOverride Sub Initialize(ByVal host As Object)
End Class
也可以使用接口。我更喜欢 MustInherit
(抽象) 类,因为我可以强制执行其成员的范围。但这一切都取决于情况。
System.Activator 类
毫无疑问,这是在运行时动态实例化对象的最简单、最常见的方法。
Dim MyWidget As Widget = DirectCast(Activator.CreateInstance(WidgetType), Widget)
MyWidget.Initialize(...)
然而,“简单”是有代价的;它的性能是所有方法中最差的。在某些情况下,性能可能不是一个大问题。如果您只需要创建少量对象,那么这很可能没问题。但是,如果您需要频繁创建大量动态对象,System.Activator
不是一个好的选择。
System.Reflection - GetConstructor
另一种简单的方法是使用反射。以下显示了如何通过获取其 ConstructorInfo
来实例化对象
Dim CtorInfo As ConstructorInfo = WidgetType.GetConstructor(System.Type.EmptyTypes)
Dim MyWidget As Widget = DirectCast(CtorInfo.Invoke(New Object() {}), Widget)
调用构造函数(如果已缓存)的性能优于 System.Activator
。因此,为了避免每次需要新实例时都调用 GetConstructor
,我们使用 Hashtable
来缓存 Type
,在这种情况下,是 ContructorInfo
。尽管微不足道,但 Hashtable
查找会产生轻微的性能损失。
System.Reflection - GetMethod + Delegate
还有一种方法是创建并缓存一个 Delegate
。如果没有 Hashtable
/Cache 查找,此方法的性能几乎与直接实例化一样快,但存在一些严重问题。以下显示了如何实现
Public Delegate Function GetInstanceDelegate() As Widget
Dim GetInstanceMethod As MethodInfo = _
WidgetType.GetMethod( _
"GetWidgetInstance", _
BindingFlags.Public Or BindingFlags.Static)
Dim GetInstanceDelegate As GetInstanceDelegate = _
DirectCast( _
System.Delegate.CreateDelegate( _
GetType(GetInstanceDelegate), GetInstanceMethod), _
GetInstanceDelegate)
这里的问题在于,您必须在每个 Widget
中有一个 Shared
(C# 中为 static
) 方法来返回正确的实例类型。并且,它必须在所有 Widget
中都相同。
Public Class SuperWidget
Inherits Widget
' ...
Public Shared Function GetWidgetInstance() As Widget
Return New SuperWidget
End Function
End Class
由于它是 Shared
,您无法通过 Interface
或基类来强制执行它的存在。您只能假设它存在并且拼写正确。如果您正在创建要分发的 API 或应用程序,这不是一种实用的方法。但是,有一个解决方案……
System.Runtime.Serialization - FormatterServices.GetUninitializedObject
(附录) 在深入研究 Microsoft 的 ObjectBuilder DI 框架时,我偶然发现了 FormatterServices
类。我很好奇正在使用哪些方法来实例化对象。经过调查,我发现当对象被反序列化时,反序列化器会创建一个未初始化的实例。换句话说,它在不调用任何构造函数的情况下被实例化。很棒!随后,对象的成员会用先前序列化的值重新填充。
在前面的示例中,每个 Widget
中的一个 Shared
方法返回了相应类型的实例。现在,我们可以实质上实现相同甚至更好的性能,而无需一个无法强制执行的 Shared
方法。这允许 GetWidgetInstance()
方法由 Interface
或基类中的 MustOverride
来强制执行。以下是实现方法
将 MustOverride Function GetInstance()
添加到基类。
Public MustInherit Class Widget
Public MustOverride Sub Initialize(ByVal host As Object)
Public MustOverride Function GetInstance() As Widget
End Class
根据需要重写它,并返回一个实例。如果需要其他初始化参数,可以在基类中的 GetInstance()
方法中添加它们,例如 Public MustOverride Function GetInstance(host as Object) As Widget
。
Public Class WonderWidget
Inherits Widget
' ...
Public Overrides Function GetInstance() As Widget
Return New WonderWidget
End Function
End Class
每个 Widget
都成为自己的工厂。不完全是动态的,因为您仍然需要硬编码要返回的类型。不过,这里的好处是您可以轻松地在返回 Widget
之前运行其他代码、调用方法和设置属性。下面的 WidgetFactory
,它本身就是一个 Widget
,只是一个特定 Widget
类型的实例。但是,没有调用构造函数,因此任何初始化代码都从未执行。因此,调用其他成员可能会导致异常。但对于我们的目的来说没关系。一个通用的工厂类可以隐藏细节,以防止这种情况发生,也可以缓存每个未初始化的 Widget
。要批量创建实例,您只需...
Dim WidgetFactory As Widget = DirectCast( _
FormatterServices.GetUninitializedObject(WidgetType), Widget)
Dim MyWidget As Widget = WidgetFactory.GetInstance()
System.Reflection.Emit - 运行时动态工厂
为了获得与 Delegate
方法相同的性能,并通过基类或 Interface
来强制执行我们的成员,我们可以使用 Reflection.Emit
命名空间为每个 Widget
生成运行时“机器”程序集。每台机器只返回一种 Widget
类型。WidgetFactory
负责创建机器并从中返回 Widget
。每台机器都缓存在 Hashtable
中,以 Widget
的 Type
作为键。为了获得最大性能,在启动时会预先扫描程序集(或 AppDomain)以查找 Widget
。因为机器是预先构建的,所以可以避免在每次调用时进行额外的 Hashtable
查找来检查它们是否存在。要构建一个 WidgetInstanceMachine
,我们首先需要定义一个 Interface
。
Public Interface IWidgetInstanceMachine
Function GetInstance() As Widget
End Interface
以下代码显示了如何通过构建运行时程序集来创建每台机器
Private Shared Function GetWidgetMachine(ByVal widgetType As Type) As IWidgetInstanceMachine
Dim Name As String = widgetType.FullName & "InstanceMachine"
Dim AssemblyName As AssemblyName = New AssemblyName
AssemblyName.Name = Name
Dim SavePath As String = "C:\Temp" ' Uncomment below to save.
Dim TypeName As String = widgetType.Name & "Machine"
' Define the assembly...
Dim AssemblyBuilder As AssemblyBuilder = _
Thread.GetDomain().DefineDynamicAssembly( _
AssemblyName, _
AssemblyBuilderAccess.RunAndSave, _
SavePath)
Dim ModuleBuilder As ModuleBuilder = _
AssemblyBuilder.DefineDynamicModule("Machine", Name & ".dll")
' Define the class "Public Class xxxWidgetMachine"
Dim TypeBuilder As TypeBuilder = _
ModuleBuilder.DefineType(TypeName, TypeAttributes.Public)
' Implement the interface "Implements IWidgetInstanceMachine"
TypeBuilder.AddInterfaceImplementation(GetType(IWidgetInstanceMachine))
' Define the constructor "Public Sub New()"
TypeBuilder.DefineDefaultConstructor(MethodAttributes.Public)
' Define the input parameter array. In this case there are none but,
' If you want to pass parameters, simply list the Types in
' the braces: {GetType(String), GetType(Integer)}
Dim InParamTypes As Type() = New Type() {}
' Define the return Type... Note: This MUST match the return Type
' defined in the Intereface, otherwise you'll get a runtime error
' indicating that the interface has not been implemented.
Dim ReturnType As Type = GetType(Widget)
' Define the "GetInstance()" method...
Dim GetInstance_MethodBuilder As MethodBuilder = _
TypeBuilder.DefineMethod("GetInstance", _
MethodAttributes.Public Or MethodAttributes.Virtual, _
ReturnType, InParamTypes)
' Get the Widget's constructor. In this case, we have no input
' parameters so the derived class', System.Object, contructor
' will be called.
Dim WidgetCtor As ConstructorInfo = widgetType.GetConstructors()(0)
' Get an IL generator to emit the body of the GetInstance method...
Dim GetInstance_ILGenerator As ILGenerator = _
GetInstance_MethodBuilder.GetILGenerator
' Two OpCode's emit the body of the GetInstance method:
' "Return New xxxWidget"
' ----------------------------------------
' 1. Creates the Widget and pop's it onto the stack...
GetInstance_ILGenerator.Emit(OpCodes.Newobj, WidgetCtor)
' 2. Return's the Widget from the stack...
GetInstance_ILGenerator.Emit(OpCodes.Ret)
' ----------------------------------------
TypeBuilder.CreateType()
' Uncomment to save the assembly...
'AssemblyBuilder.Save(Name & ".dll")
Return DirectCast(AssemblyBuilder.CreateInstance(TypeName), IWidgetInstanceMachine)
End Function
由于我对 MSIL 的了解有限,我使用了 Reflector 并进行了一些试错,才得到了想要的结果。如果我们保存并打开其中一个机器程序集,我们将看到以下内容...
另一个有用的工具是 DILE。您可以看到 ILGenerator 发射的两个 OpCodes。创建/硬编码一个单独的机器,然后编译并查看 IL,可以显示您需要发射的 OpCodes 以在运行时重现该机器。
最后,当所有机器都运行时,WidgetFactory
可以检索一台机器并从中获取一个 Widget
。
Public Class WidgetFactory
Public Shared Function GetWidget(ByVal widgetType As Type) As Widget
Return DirectCast(_MachineCache(widgetType), _
IWidgetInstanceMachine).GetInstance()
End Function
' ...
End Class
关注点
IWidgetInstanceMachine
是一个非常简单的实现。通过一点额外的努力,您可以通过机器的 GetInstance(param1, ...)
方法传递参数,以在创建 Widget
时使用。这允许您创建更复杂的机器,这些机器本身的行为更像工厂。
参考文献和鸣谢
- 一篇意图相似的优秀文章 - Fast Dynamic Property Access with C# - 给了我所需的起点。
- 一篇有些过时但仍然非常出色的相关讨论可以在 Performance of Dynamic Object Instantiation 中找到。我还使用了部分性能基准测试代码,并从 Delegate 方法中获得了启发。
- Lutz Roeder 的 Reflector 是继切片面包之后最伟大的发明。
- DILE,一个 MSIL 编辑器和调试器,也是深入了解代码底层的好方法。
历史
- 2007/10/23 - 初次创建。
- 2007/11/27 - 添加了
FormatterServices.GetUninitializedObject
方法并更新了演示项目。