Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / MSIL
Article

Dynamic Objects, Factories, and Runtime Machines to Boost Performance

Rate me:
Please Sign up or sign in to vote.
4.79/5 (10 votes)
27 Nov 2007CPOL7 min read 47.9K   303   46   4
An examination of dynamic object instantiation and runtime machines to boost performance.

TestRun

Introduction

There are a number ways to dynamically instantiate objects at runtime. There are some methods built-in to the .NET Framework, such as the System.Activator class, Reflection, and ways you can brew your own. Each with its own consequences and payoff's. During my search for the fastest way to dynamically instantiate objects, I consolidated information about the various methods, their practicality, and respective performance. Please note that this article applies to .NET 1.1 - my knowledge of 2.0 is still limited as my primary work environment is (more like "is struggling to be") a .NET 1.1 shop. In the more current versions of .NET, there are additional ways to achieve what I present in this article.

This article assumes an understanding of Reflection and a bit about MSIL. I'm not an MSIL guru in any form, so I won't spend much time on the subject. I had to experiment quite a bit, but luckily, there are some great tools to make experimenting easier. See the References section for details.

Background

The contents of this article are not new. These performance benchmarks have been performed many times. The References section lists articles that I've read and used to compile this article. My intent is to provide a comparison based on my understanding, as well as to demonstrate how a factory can be created to make the creation of dynamic objects as fast as possible. I look forward to comments and suggestions.

Common Methods of Creating Dynamic Objects

The fastest way to instantiate an object is by directly calling its constructor.

VB
Dim MyWidget As New SuperWidget

This, however, does not allow us to write generic, type-independent code. There are often times that a Type is not know until runtime, so let's examine the following methods of dynamic object creation and their respective performance. The screenshot above shows my benchmark results. If you're already familiar with the following, you may want to skip ahead to the Dynamic Factory section.

  • System.Activator - Activator.CreateInstance(type As System.Type)
  • Reflection - ConstructorInfo.Invoke()
  • Reflection - MethodInfo and a Delegate
  • System.Runtime.Serialization - FormatterServices.GetUninitializedObject(type As System.Type)
  • System.Reflection.Emit - Emit dynamic assemblies that create object instances!

I'll use the following Type in the code examples below:

VB
Dim WidgetType As Type = Type.GetType("SuperWidget")

And, all our Widgets derive from:

VB
Public MustInherit Class Widget

    Public MustOverride Sub Initialize(ByVal host As Object)

End Class

An Interface could also be used. I prefer a MustInherit (abstract) class as I can enforce the scope of its members. But, it's all a mater of what works for the situation.

The System.Activator Class:

Without a doubt, the easiest, most common way to dynamically instantiate objects at runtime.

VB
Dim MyWidget As Widget = DirectCast(Activator.CreateInstance(WidgetType), Widget) 
MyWidget.Initialize(...)

Though, 'easy' comes at a price; it's by far the worst performing. In certain cases, performance may not be a big concern. If you only need to create a few objects here and there, then it's most likely OK. However, if you have a need to frequently create many dynamic objects, the System.Activator is not a good choice.

System.Reflection - GetConstructor:

Another simple method is to use Reflection. The following shows how to instantiate an object by obtaining its ConstructorInfo:

VB
Dim CtorInfo As ConstructorInfo = WidgetType.GetConstructor(System.Type.EmptyTypes)
Dim MyWidget As Widget = DirectCast(CtorInfo.Invoke(New Object() {}), Widget)

Invoking the constructor, if cached, out-performs the System.Activator. So, to avoid calling GetConstructor each time we need a new instance, a Hashtable is used for caching the Type and, in this case, the ContructorInfo. While insignificant, the Hashtable lookups will incur a small performance hit.

System.Reflection - GetMethod + Delegate:

Yet another method is to create and cache a Delegate. Without a Hashtable/Cache lookup, this method is just about as fast as direct instantiation, but has some serious problems. The following shows how it's done:

VB
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)

The problem here is that you must have a Shared (static in C#) method in each Widget to return the correct instance type. And, it must be the same in all your Widgets.

VB
Public Class SuperWidget
    Inherits Widget

    ' ...

    Public Shared Function GetWidgetInstance() As Widget
        Return New SuperWidget
    End Function

End Class

Because it's Shared, you cannot enforce its existence with an Interface or base class. You just have to assume it's there and spelled correctly. Not a practical approach if you are creating an API or application for distribution. But, there is a solution...

System.Runtime.Serialization - FormatterServices.GetUninitializedObject:

(Addendum) I stumbled across the FormatterServices class while digging through Microsoft's ObjectBuilder DI Framework. I was curious as to what methods were being used to instantiate objects. Upon investigation, I found that when an object is de-serialized, an uninitialized instance of it is created by the deserializer. In other words, it's instantiated without calling any of its constructors. Neat! Subsequently, the object's members are repopulated with the previously serialized values.

In the previous example, a Shared method within each Widget returned an instance of the respective type. Now, we can essentially achieve the same or better performance without an unenforceable Shared method. This allows the GetWidgetInstance() method to be enforced by an Interface or MustOverride in the base class. Here's how it's done:

Add the MustOverride Function GetInstance() to the base class.

VB
Public MustInherit Class Widget

    Public MustOverride Sub Initialize(ByVal host As Object)

    Public MustOverride Function GetInstance() As Widget

End Class

Override it as required, and return an instance. If other initialization parameters are required, they can be added to the GetInstance() method in the base class, i.e., Public MustOverride Function GetInstance(host as Object) As Widget.

VB
Public Class WonderWidget
    Inherits Widget

    ' ...

    Public Overrides Function GetInstance() As Widget
        Return New WonderWidget
    End Function

End Class

Each Widget becomes its own factory. Not entirely dynamic, as you still need to hard code the type being returned. Though, the nice thing here is that you can easily run additional code, call methods, and set properties before returning the Widget. The following WidgetFactory, a Widget itself, is just an instance of a specific Widget type. However, no constructor calls were made, so any initialization code was never executed. Because of this, it's possible that calls to other members will result in an exception. But that's okay for our purposes. A generic factory class can conceal the details to prevent this from happening as well as cache each uninitialized Widget. To pump out instances, you simply...

VB
Dim WidgetFactory As Widget = DirectCast( _
    FormatterServices.GetUninitializedObject(WidgetType), Widget)
Dim MyWidget As Widget = WidgetFactory.GetInstance()

System.Reflection.Emit - The Runtime Dynamic Factory:

To achieve the same performance as the Delegate method and still enforce our members by way of a base class or Interface, we can use the Reflection.Emit namespace to generate runtime "machine" assemblies for each Widget. Each machine returns only a single type of Widget. A WidgetFactory is responsible for creating the machines and returning Widgets from them. Each machine is cached in a Hashtable, keyed by the Type of Widget it creates. To achieve maximum performance, the assembly (or AppDomain) is pre-scanned for Widgets at startup. Because the machines are pre-built, an extra Hashtable lookup to check for their existence on every call is avoided. To build a WidgetInstanceMachine, we first need to define an Interface.

VB
Public Interface IWidgetInstanceMachine

    Function GetInstance() As Widget

End Interface

The following code shows how each machine is created by building a runtime assembly:

VB
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

Because of my limited knowledge of MSIL, I used Reflector in addition to a bit of trial and error to get the desired results. If we save and open one of the machine assemblies, we'll see the following...

The Machine

Another helpful tool is DILE. You can see the two OpCodes that were emitted by the ILGenerator. Creating/hard-coding a single machine, then compiling and reviewing the IL shows the OpCodes you need to emit to reproduce the machine at runtime.

Under the hood

And finally, when all the machines are running, the WidgetFactory can retrieve a machine and get a Widget from it.

VB
Public Class WidgetFactory

    Public Shared Function GetWidget(ByVal widgetType As Type) As Widget
        Return DirectCast(_MachineCache(widgetType), _
               IWidgetInstanceMachine).GetInstance()
    End Function

    ' ...

End Class

Points of Interest

The IWidgetInstanceMachine is a very simple implementation. With a little extra effort, you can pass parameters through the machine's GetInstance(param1, ...) method to be used when a Widget is created. This allows you to create more complex machines that behave more like factories themselves.

References & Credits

  • An excellent article with a similar intent - Fast Dynamic Property Access with C# - Gave me the start I needed.
  • A somewhat dated, but ever still excellent discussion on the topic can be found at Performance of Dynamic Object Instantiation. I also used pieces of the performance benchmarking code, and was enlightened by the Delegate approach.
  • Lutz Roeder's Reflector is the best thing since sliced bread.
  • DILE, an MSIL editor and debugger, is also a nice way to get all the way under the hood of your code.

History

  • 10/23/2007 - Initial creation.
  • 11/27/2007 - Added the FormatterServices.GetUninitializedObject method and updated the demo project.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) @Everywhere
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralVery Interesting Pin
Ben Daniel23-Oct-07 13:51
Ben Daniel23-Oct-07 13:51 
GeneralRe: Very Interesting Pin
Philip Liebscher23-Oct-07 20:41
Philip Liebscher23-Oct-07 20:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.