65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (9投票s)

2007 年 10 月 23 日

CPOL

7分钟阅读

viewsIcon

53085

downloadIcon

321

对动态对象实例化和运行时机器以提高性能的考察。

TestRun

引言

有许多方法可以在运行时动态地实例化对象。 .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()
  • 反射 - MethodInfoDelegate
  • 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 中,以 WidgetType 作为键。为了获得最大性能,在启动时会预先扫描程序集(或 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 并进行了一些试错,才得到了想要的结果。如果我们保存并打开其中一个机器程序集,我们将看到以下内容...

The Machine

另一个有用的工具是 DILE。您可以看到 ILGenerator 发射的两个 OpCodes。创建/硬编码一个单独的机器,然后编译并查看 IL,可以显示您需要发射的 OpCodes 以在运行时重现该机器。

Under the hood

最后,当所有机器都运行时,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 方法并更新了演示项目。
© . All rights reserved.