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

动态... 但速度飞快:三个猴子、一只狼以及 DynamicMethod 和 ILGenerator 类的故事

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (259投票s)

2007 年 7 月 7 日

BSD

6分钟阅读

viewsIcon

1051879

downloadIcon

1253

如何使用 DynamicMethod 和 ILGenerator 类在运行时创建动态代码,其性能优于反射。

猴子一号

从前,有三只小代码猴子。一号猴子在第七街和宾夕法尼亚大街的草料厂工作。有一天,一号猴子老板 B. B. Wolf 给了他一项新任务。人力资源部门的应用程序正在升级到 C#。一号猴子的任务是编写代码,从数据库中填充 Person 类。这只猴子立即投入工作,很快就写出了与以下代码类似的代码(出于法律原因,实际代码无法使用)。

C#

public class ManualBuilder
{
    public Person Build(SqlDataReader reader)
    {
        Person person = new Person();

        if (!reader.IsDBNull(0))
        {
            person.ID = (Guid)reader[0];
        }

        if (!reader.IsDBNull(1))
        {
            person.Name = (string)reader[1];
        }

        if (!reader.IsDBNull(2))
        {
            person.Kids = (int)reader[2];
        }

        if (!reader.IsDBNull(3))
        {
            person.Active = (bool)reader[3];
        }

        if (!reader.IsDBNull(4))
        {
            person.DateOfBirth = (DateTime)reader[4];
        }

        return person;
    }
}

VB

Public Class ManualBuilder

    Public Function Build(ByVal reader As SqlDataReader) As Person
        Dim person As Person = New Person()

        If Not reader.IsDBNull(0) Then
            person.ID = CType(reader(0), Guid)
        End If

        If Not reader.IsDBNull(1) Then
            person.Name = CType(reader(1), String)
        End If

        If Not reader.IsDBNull(2) Then
            person.Kids = CType(reader(2), Integer)
        End If

        If Not reader.IsDBNull(3) Then
            person.Active = CType(reader(3), Boolean)
        End If

        If Not reader.IsDBNull(4) Then
            person.DateOfBirth = CType(reader(4), DateTime)
        End If

        Return person
    End Function

End Class

起初,这段代码运行良好。它干净利落,而且速度很快。然而,人力资源部主管决定,新软件应该有一些额外的功能。每个新功能似乎都需要向 Person 表添加字段,创建新表,或者将字段从 Person 表移到新表之一。每次引入新功能时,这只猴子都必须编辑他的代码或为新表编写映射代码。看来,这只猴子总是成为任何新功能开发的瓶颈。有一天,Wolf 先生叫猴子到他的办公室。Wolf 先生哼哧哼哧地,就把猴子解雇了。

猴子二号

后来,Wolf 先生因为涉嫌对 Pig 太太行为不端而被草料厂解雇。他最终在木材厂找到了新工作,而猴子二号恰好在那里工作。Wolf 先生之所以被木材厂雇佣,正是因为他有升级人力资源应用程序的经验,而这恰恰是木材厂即将开始的项目类型。不出所料,猴子二号被派去负责编写代码,从数据库中填充 Person 类。Wolf 先生告知了猴子一号的命运,并含蓄地暗示,如果猴子二号不能拿出更灵活的解决方案,他的命运也将如此。这只猴子考虑了一会儿,写出了与以下代码类似的东西。

C#

public class ReflectionBuilder<t>
{
    private PropertyInfo[] properties;

    private ReflectionBuilder() { }

    public T Build(SqlDataReader reader)
    {
        T result = (T)Activator.CreateInstance(typeof(T));

        for (int i = 0; i < reader.FieldCount; i++)
        {
            if (properties[i] != null && !reader.IsDBNull(i))
            {
                properties[i].SetValue(result, reader[i], null);
            }
        }

        return result;
    }

    public static ReflectionBuilder<t> CreateBuilder(SqlDataReader reader)
    {
        ReflectionBuilder<t> result = new ReflectionBuilder<t>();

        result.properties = new PropertyInfo[reader.FieldCount];
        for (int i = 0; i < reader.FieldCount; i++)
        {
            result.properties[i] = typeof(T).GetProperty(reader.GetName(i));
        }

        return result;
    }
}</t>

VB

Public Class ReflectionBuilder(Of T)
    Private properties() As PropertyInfo

    Private Sub ReflectionBuilder()

    End Sub

    Public Function Build(ByVal reader As SqlDataReader) As T
        Dim result As T = CType(Activator.CreateInstance(GetType(T)), T)
        Dim i As Integer

        For i = 0 To reader.FieldCount - 1
            If Not properties(i) Is Nothing And Not reader.IsDBNull(i) Then
                properties(i).SetValue(result, reader(i), Nothing)
            End If
        Next

        Return result
    End Function

    Public Shared Function CreateBuilder(ByVal reader As SqlDataReader) _
            As ReflectionBuilder(Of T)
        Dim result As ReflectionBuilder(Of T) = New ReflectionBuilder(Of T)()
        Dim i As Integer
        ReDim result.properties(0 To reader.FieldCount)

        For i = 0 To reader.FieldCount - 1
            result.properties(i) = GetType(T).GetProperty(reader.GetName(i))
        Next

        Return result

    End Function

End Class

这个解决方案比第一只猴子的解决方案好得多。正如你可能猜到的,“升级”人力资源应用程序的需求一直在变化。“添加此功能”、“删除此功能”、“将此项移至此处”、“将此项移至那里”。这些似乎都不重要。猴子二号使用反射意味着他的代码可以自动识别更改。更好的是,当创建新表和对象时,可以使用相同的代码,无需任何额外更改。一切进展得都非常顺利。猴子二号确信自己将获得一次重要的晋升。

但之后发生了难以想象的事情……应用程序上线了。突然,Wolf 先生接到了无数来自不满的人力资源员工的电话,抱怨新应用程序运行得太慢了。几周后,Wolf 先生因其无能而被解雇,木材厂恢复使用他们旧的人力资源软件。然而,Wolf 先生在离开前确实设法解雇了猴子二号。

猴子三号

尽管 Wolf 先生在项目管理方面显得有些笨拙,但他很快就在砖厂找到了新工作。砖厂正在迁移他们旧的人力资源软件,并认为 Wolf 先生的“专业知识”会带来好处。巧合的是,砖厂也是猴子三号的雇主。Wolf 先生再次将使用数据库中的数据加载 Person 类的任务分配给猴子三号,并再次暗示猴子的继续就业取决于他是否会造成猴子一号和二号所遇到的任何问题。猴子三号进行了一些研究,偶然发现了 .NET 2.0 中的 DynamicMethodILGenerator 类。这些类允许猴子在运行时动态创建和编译代码。这将让他两全其美。他的代码可以像猴子二号那样动态,但由于它实际上是编译过的,所以速度会和猴子一号一样快。

他做了一些实验。缺点是动态代码需要用 IL(中间语言)而不是 C# 来编写。然而,通过少量谷歌搜索、使用 .NET SDK 中的 ildasm.exe 反编译代码,以及一些经典的试错,这只猴子能够创建出类似以下的代码。

C#

public class DynamicBuilder<T>
{
    private static readonly MethodInfo getValueMethod = 
        typeof(IDataRecord).GetMethod("get_Item", new Type[] { typeof(int) });
    private static readonly MethodInfo isDBNullMethod = 
        typeof(IDataRecord).GetMethod("IsDBNull", new Type[] { typeof(int) });
    private delegate T Load(IDataRecord dataRecord);
    private Load handler;

    private DynamicBuilder() { }

    public T Build(IDataRecord dataRecord)
    {
        return handler(dataRecord);
    }

    public static DynamicBuilder<T> CreateBuilder(IDataRecord dataRecord)
    {
        DynamicBuilder<T> dynamicBuilder = new DynamicBuilder<T>();

        DynamicMethod method = new DynamicMethod("DynamicCreate", typeof(T), 
                new Type[] { typeof(IDataRecord) }, typeof(T), true);
        ILGenerator generator = method.GetILGenerator();

        LocalBuilder result = generator.DeclareLocal(typeof(T));
        generator.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));
        generator.Emit(OpCodes.Stloc, result);

        for (int i = 0; i < dataRecord.FieldCount; i++)
        {
            PropertyInfo propertyInfo = typeof(T).GetProperty(dataRecord.GetName(i));
            Label endIfLabel = generator.DefineLabel();

            if (propertyInfo != null && propertyInfo.GetSetMethod() != null)
            {
                generator.Emit(OpCodes.Ldarg_0);
                generator.Emit(OpCodes.Ldc_I4, i);
                generator.Emit(OpCodes.Callvirt, isDBNullMethod);
                generator.Emit(OpCodes.Brtrue, endIfLabel);

                generator.Emit(OpCodes.Ldloc, result);
                generator.Emit(OpCodes.Ldarg_0);
                generator.Emit(OpCodes.Ldc_I4, i);
                generator.Emit(OpCodes.Callvirt, getValueMethod);
                generator.Emit(OpCodes.Unbox_Any, dataRecord.GetFieldType(i));
                generator.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());

                generator.MarkLabel(endIfLabel);
            }
        }

        generator.Emit(OpCodes.Ldloc, result);
        generator.Emit(OpCodes.Ret);

        dynamicBuilder.handler = (Load)method.CreateDelegate(typeof(Load));
        return dynamicBuilder;
    }
}

VB

Public Class DynamicBuilder(Of T)
    Private Shared ReadOnly getValueMethod As MethodInfo = _
        GetType(IDataRecord).GetMethod("get_Item", New Type() {GetType(Integer)})
    Private Shared ReadOnly isDBNullMethod As MethodInfo = _
        GetType(IDataRecord).GetMethod("IsDBNull", New Type() {GetType(Integer)})
    Private Delegate Function Load(ByVal dataRecord As IDataRecord) As T
    Private handler As Load

    Private Sub DynamicBuilder()

    End Sub

    Public Function Build(ByVal dataRecord As IDataRecord) As T
        Return handler(dataRecord)
    End Function


    Public Shared Function CreateBuilder(ByVal dataRecord As IDataRecord) _
        As DynamicBuilder(Of T)
        Dim dynamicBuilder As DynamicBuilder(Of T) = New DynamicBuilder(Of T)()
        Dim i As Integer

        Dim method As DynamicMethod = New DynamicMethod("DynamicCreate", GetType(T), _
            New Type() {GetType(IDataRecord)}, GetType(T), True)
        Dim generator As ILGenerator = method.GetILGenerator()

        Dim result As LocalBuilder = generator.DeclareLocal(GetType(T))
        generator.Emit(OpCodes.Newobj, GetType(T).GetConstructor(Type.EmptyTypes))
        generator.Emit(OpCodes.Stloc, result)

        For i = 0 To dataRecord.FieldCount - 1
            Dim propertyInfo As PropertyInfo = _
                GetType(T).GetProperty(dataRecord.GetName(i))
            Dim endIfLabel As Label = generator.DefineLabel()

            If Not propertyInfo Is Nothing Then
                If Not propertyInfo.GetSetMethod() Is Nothing Then
                    generator.Emit(OpCodes.Ldarg_0)
                    generator.Emit(OpCodes.Ldc_I4, i)
                    generator.Emit(OpCodes.Callvirt, isDBNullMethod)
                    generator.Emit(OpCodes.Brtrue, endIfLabel)

                    generator.Emit(OpCodes.Ldloc, result)
                    generator.Emit(OpCodes.Ldarg_0)
                    generator.Emit(OpCodes.Ldc_I4, i)
                    generator.Emit(OpCodes.Callvirt, getValueMethod)
                    generator.Emit(OpCodes.Unbox_Any, dataRecord.GetFieldType(i))
                    generator.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod())

                    generator.MarkLabel(endIfLabel)
                End If
            End If
        Next

        generator.Emit(OpCodes.Ldloc, result)
        generator.Emit(OpCodes.Ret)

        dynamicBuilder.handler = CType(method.CreateDelegate(GetType(Load)), Load)
        Return dynamicBuilder
    End Function
End Class

Wolf 先生对此表示怀疑,所以猴子三号尽力解释发生了什么。

CreateBuilder 的前几行实例化了 DynamicMethodILGenerator 类。简而言之,它创建了一个名为 DynamicCreate 的新 static 方法,并将该方法添加到传入的对象类型中,即示例中的 Person 类。该方法接受 SqlDataReader 并返回正确对象的实例。如果这是非动态代码,你可能会这样调用它。

C#

Person myPerson = Person.DynamicCreate(mySqlDataReader);

VB

dim myPerson as Person = Person.DynamicCreate(mySqlDataReader)

下一行代码生成了一个通用类型变量。所以,这个,

LocalBuilder result = generator.DeclareLocal(typeof(T));

在非动态代码中,这将是这样。

C#

Person myPerson;

VB

Dim myPerson as Person

下一段代码实例化了请求类型的对象并将其存储在局部变量中。

generator.Emit(OpCodes.Newobj, typeof(T).GetConstructor(Type.EmptyTypes));
generator.Emit(OpCodes.Stloc, result);

在非动态代码中,这将是这样。

C#

myPerson = new Person();

VB

myPerson = new Person()

然后,代码遍历数据读取器中的字段,查找类型中匹配的属性。找到匹配项后,代码会检查数据读取器中的值是否为 null

C#

generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldc_I4, i);
generator.Emit(OpCodes.Callvirt, isDBNullMethod);
generator.Emit(OpCodes.Brtrue, endIfLabel);

...

generator.MarkLabel(endIfLabel);

if (!mySqlDataReader.IsDBNull(1))
{
    ...
}

VB

If Not mySqlDataReader.IsDBNull(1) Then
    ...
End If

如果数据读取器中的值不为 null,代码将该值设置到对象上。

generator.Emit(OpCodes.Ldloc, result);
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldc_I4, i);
generator.Emit(OpCodes.Callvirt, getValueMethod);
generator.Emit(OpCodes.Unbox_Any, dataRecord.GetFieldType(i));
generator.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());

同样,在非动态代码中,这将是这样。

C#

myPerson.Name = (string)mySqlDataReader[1];

VB

myPerson.Name = CType(mySqlDataReader(1), string)

代码的最后一部分返回局部变量的值。

C#

generator.Emit(OpCodes.Ldloc_0);
generator.Emit(OpCodes.Ret);

return myPerson;

VB

return myPerson

然后代码返回一个指向 delegatehandler。当调用此 handler 时,它会调用动态生成的代码,这可以在以下代码中看到。

C#

public T Build(SqlDataReader reader)
{
    return handler(reader);
}

VB

Public Function Build(ByVal dataRecord As IDataRecord) As T
    Return handler(dataRecord)
End Function

Wolf 先生对这一切的含义一无所知,但不想显得愚蠢,他说:“听起来很有希望,但在我们继续之前,先做一些基准测试吧。”猴子三号很快就组织了一项测试,以使用所有三种方法。每个样本将从 Person 表中加载三百万行。结果如下。

Screenshot - Perf.jpg

根据这些结果,Wolf 先生让猴子三号实施了他的解决方案。开发进展顺利。现场发布效果更好。这个项目取得了巨大的成功。性能良好,而且接近预算。Wolf 先生获得了巨额奖金,提前退休,并搬到了一个小私人岛屿。猴子三号后来被裁员,目前失业。

保持简单,猴子

注意:本文是一个极度简化的版本。代码旨在介绍动态运行时代码生成,而不是一个功能完善的解决方案。话虽如此,如果你仔细而审慎地应用这里提出的思想,你应该能够像猴子三号一样成功。

历史上的猴子

  • 2007 年 7 月 7 日 - 发布了原始文章版本
  • 2007 年 7 月 13 日 - 文章经过编辑并移至 CodeProject 文章主基地
  • 2007 年 7 月 24 日 - 已更新
  • 2008 年 7 月 20 日 - 更新以处理 DBNull,处理任何 IDataRecord(而不仅仅是 SqlDataReader),并添加了 VB。
© . All rights reserved.