一个动态程序集中的多个类型比具有每个类型的多个动态程序集慢

时间:2017-11-14 21:48:19

标签: c# reflection.emit

所以我通过DefineDynamicAssembly发出了一些动态代理,在测试时我发现:

  • 每个动态程序集一种类型:快速,但使用大量内存
  • 一个动态程序集中的所有类型:非常慢,但使用的内存要少得多

在我的测试中,我生成了10,000种类型,并且每种类型的一种类型代码运行速度提高了大约8-10倍。内存使用情况完全符合我的预期,但是如何生成类型的时间要长得多?

编辑:添加了一些示例代码。

一个集会:

var an = new AssemblyName( "Foo" );
var ab = AppDomain.CurrentDomain.DefineDynamicAssembly( an, AssemblyBuilderAccess.Run );
var mb = ab.DefineDynamicModule( "Bar" );

for( int i = 0; i < 10000; i++ )
{                
    var tb = mb.DefineType( "Baz" + i.ToString( "000" ) );
    var met = tb.DefineMethod( "Qux", MethodAttributes.Public );
    met.SetReturnType( typeof( int ) );

    var ilg = met.GetILGenerator();
    ilg.Emit( OpCodes.Ldc_I4, 4711 );
    ilg.Emit( OpCodes.Ret );

    tb.CreateType();
}

每种类型一个装配:

 for( int i = 0; i < 10000; i++ )
 {
    var an = new AssemblyName( "Foo" );
    var ab = AppDomain.CurrentDomain.DefineDynamicAssembly( an,
                                                            AssemblyBuilderAccess.Run );
    var mb = ab.DefineDynamicModule( "Bar" );

    var tb = mb.DefineType( "Baz" + i.ToString( "000" ) );
    var met = tb.DefineMethod( "Qux", MethodAttributes.Public );
    met.SetReturnType( typeof( int ) );

    var ilg = met.GetILGenerator();
    ilg.Emit( OpCodes.Ldc_I4, 4711 );
    ilg.Emit( OpCodes.Ret );

    tb.CreateType();
}

2 个答案:

答案 0 :(得分:8)

在使用C#7.0的LINQPad中的PC上,我得到一个大约8.8秒的程序集,每个类型一个程序集大约2.6秒。一个程序集中的大部分时间都在DefineTypeCreateType,而时间主要在DefineDynamicAssembly + DefineDynamicModule

DefineType检查没有名称冲突,这是Dictionary查找。如果Dictionary为空,则表示检查null

大部分时间花费在CreateType上,但我看不出位置,但似乎需要额外的时间将类型添加到单个模块中。

创建多个模块会降低整个过程的速度,但大部分时间都花在创建模块上,并且在DefineType中,必须扫描每个模块以获得重复,因此现在增加到10,000 {{1}检查。对于每种类型的唯一模块,null非常快。

答案 1 :(得分:7)

在我的检查中,为什么在一个程序集中定义多个模块比使用一个模块创建一个新程序集要慢,使用这些代码:

单装配场景:

        var an = new AssemblyName("Foo");
        var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
        for (int i = 0; i < 10000; i++)
        {
            ab.DefineDynamicModule("Bar" + i.ToString("000"));
        }

多装配场景:

        var an = new AssemblyName("Foo");
        for (int i = 0; i < 10000; i++)
        {
            var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
            ab.DefineDynamicModule("Bar");
        }
  1. 我发现时间约为20%(多个程序集示例中为50%),底层代码会遍历所有模块名称以检查是否存在冲突。这部分是可以理解和预期的。
  2. 当使用一个组件时,另外60%-80%的时间,CLI的DefineDynamicModule()处于压力之下。但是,当使用多个程序集时,永远不会调用此方法;相反,其他方法负责剩余的50%。
  3. 让我们深入了解CLI的ECMA-335文档。

      

    II.6 程序集是一个部署为一个单元的一个或多个文件的集合。

         

    第140页

    所以我们现在明白,程序集本质上是一个包,而模块是主要组件。话虽如此:

      

    II.6 模块是包含此处指定格式的可执行内容的单个文件。如果模块包含清单,那么它还指定构成程序集的模块(包括其自身)。一个程序集在其所有组成文件中只应包含一个清单。

         

    第140页

    根据这些信息,我们知道在创建装配体时,我们也会自动将一个模块添加到装配体中。这就是为什么如果我们继续创建新程序集,我们永远不会在CLI的DefineDynamicModule()函数上受到打击。相反,我们在CLI的GetInMemoryAssemblyModule()方法上获得了一个命中,以检索有关清单模块(自动创建的模块)的信息。

    所以这里我们有一点性能提升;使用一个组件,我们获得了10001个模块,但是使用多个组件,我们总共获得了10000个模块。虽然不多,所以这一个额外的模块不应该是这背后的主要原因。

      

    II.6.5 当一个项目在当前程序集中,但是是一个模块的一部分而不是包含清单的模块时,定义模块应该在程序集的清单中使用.module extern指令。

         

    第146页

      

    II.6.7 清单模块,每个程序集只能有一个,包含.assembly指令。要导出在程序集的任何其他模块中定义的类型,需要在程序集的清单中输入一个条目。

         

    第146页

    因此,每次创建新模块时,实际上是将新文件添加到存档,然后修改存档的第一个文件以引用新模块。基本上在单汇编代码中,我们添加10000个模块,然后我们编辑第一个模块10000次。多汇编代码不是这种情况,我们只编辑第一个自动生成的模块10000次。

    这是我们看到的开销。它在我的系统上呈指数级增长。

    (5000 = 1.5s,10000 = 6s,20000 = 25s)

    但是,对于您的代码,瓶颈是从SetMethodIL方法调用的非托管CLR的CreateTypeNoLock.CreateTypeNoLock()函数,我在文档中找不到任何关于此的内容。

    不幸的是,很难反编译和理解CLR.dll以查看实际发生的情况,因此,我们只是根据微软在此阶段发布的公开信息进行猜测。