【问题标题】:Static field access in collectible dynamic assemblies lacks performance可收集的动态程序集中的静态字段访问缺乏性能
【发布时间】:2016-02-16 17:07:31
【问题描述】:

对于动态二进制翻译模拟器,我需要生成可收集的 .NET 程序集,其中包含访问静态字段的类。但是,当在可收集程序集中使用静态字段时,与不可收集程序集相比,执行性能要低 2-3 倍。该现象不存在于 不使用静态字段的可收藏程序集。

在下面的代码中,抽象类AbstrTest 的方法MyMethod 由可收集和不可收集的动态程序集实现。使用 CreateTypeConst MyMethod 将 ulong 参数值乘以常数值 2,而使用 CreateTypeField 第二个因子取自 一个构造函数初始化的静态字段MyField

为了获得真实的结果,MyMethod 结果在 for 循环中累积。

以下是测量结果(.NET CLR 4.5/4.6):

Testing non-collectible const multiply:
Elapsed: 8721.2867 ms

Testing collectible const multiply:
Elapsed: 8696.8124 ms

Testing non-collectible field multiply:
Elapsed: 10151.6921 ms

Testing collectible field multiply:
Elapsed: 33404.4878 ms

这是我的复制代码:

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Diagnostics;

public abstract class AbstrTest {
  public abstract ulong MyMethod(ulong x);
}

public class DerivedClassBuilder {

  private static Type CreateTypeConst(string name, bool collect) {
    // Create an assembly.
    AssemblyName myAssemblyName = new AssemblyName();
    myAssemblyName.Name = name;
    AssemblyBuilder myAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
       myAssemblyName, collect ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run);

    // Create a dynamic module in Dynamic Assembly.
    ModuleBuilder myModuleBuilder = myAssembly.DefineDynamicModule(name);

    // Define a public class named "MyClass" in the assembly.
    TypeBuilder myTypeBuilder = myModuleBuilder.DefineType("MyClass", TypeAttributes.Public, typeof(AbstrTest));

    // Create the MyMethod method.
    MethodBuilder myMethodBuilder = myTypeBuilder.DefineMethod("MyMethod",
       MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
       typeof(ulong), new Type [] { typeof(ulong) });
    ILGenerator methodIL = myMethodBuilder.GetILGenerator();
    methodIL.Emit(OpCodes.Ldarg_1);
    methodIL.Emit(OpCodes.Ldc_I4_2);
    methodIL.Emit(OpCodes.Conv_U8);
    methodIL.Emit(OpCodes.Mul);
    methodIL.Emit(OpCodes.Ret);

    return myTypeBuilder.CreateType();
  }

  private static Type CreateTypeField(string name, bool collect) {
    // Create an assembly.
    AssemblyName myAssemblyName = new AssemblyName();
    myAssemblyName.Name = name;
    AssemblyBuilder myAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
       myAssemblyName, collect ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run);

    // Create a dynamic module in Dynamic Assembly.
    ModuleBuilder myModuleBuilder = myAssembly.DefineDynamicModule(name);

    // Define a public class named "MyClass" in the assembly.
    TypeBuilder myTypeBuilder = myModuleBuilder.DefineType("MyClass", TypeAttributes.Public, typeof(AbstrTest));

    // Define a private String field named "MyField" in the type.
    FieldBuilder myFieldBuilder = myTypeBuilder.DefineField("MyField",
       typeof(ulong), FieldAttributes.Private | FieldAttributes.Static);

    // Create the constructor.
    ConstructorBuilder constructor = myTypeBuilder.DefineConstructor(
       MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig,
       CallingConventions.Standard, Type.EmptyTypes);
    ConstructorInfo superConstructor = typeof(AbstrTest).GetConstructor(
       BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance,
       null, Type.EmptyTypes, null);
    ILGenerator constructorIL = constructor.GetILGenerator();
    constructorIL.Emit(OpCodes.Ldarg_0);
    constructorIL.Emit(OpCodes.Call, superConstructor);
    constructorIL.Emit(OpCodes.Ldc_I4_2);
    constructorIL.Emit(OpCodes.Conv_U8);
    constructorIL.Emit(OpCodes.Stsfld, myFieldBuilder);
    constructorIL.Emit(OpCodes.Ret);

    // Create the MyMethod method.
    MethodBuilder myMethodBuilder = myTypeBuilder.DefineMethod("MyMethod",
       MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
       typeof(ulong), new Type [] { typeof(ulong) });
    ILGenerator methodIL = myMethodBuilder.GetILGenerator();
    methodIL.Emit(OpCodes.Ldarg_1);
    methodIL.Emit(OpCodes.Ldsfld, myFieldBuilder);
    methodIL.Emit(OpCodes.Mul);
    methodIL.Emit(OpCodes.Ret);

    return myTypeBuilder.CreateType();
  }

  public static void Main() {
    ulong accu;
    Stopwatch stopwatch;
    try {
      Console.WriteLine("Testing non-collectible const multiply:");
      AbstrTest i0 = (AbstrTest)Activator.CreateInstance(
        CreateTypeConst("MyClassModule0", false));
      stopwatch = Stopwatch.StartNew();
      accu = 0;
      for (uint i = 0; i < 0xffffffff; i++)
        accu += i0.MyMethod(i);
      stopwatch.Stop();
      Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");

      Console.WriteLine("Testing collectible const multiply:");
      AbstrTest i1 = (AbstrTest)Activator.CreateInstance(
        CreateTypeConst("MyClassModule1", true));
      stopwatch = Stopwatch.StartNew();
      accu = 0;
      for (uint i = 0; i < 0xffffffff; i++)
        accu += i1.MyMethod(i);
      stopwatch.Stop();
      Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");

      Console.WriteLine("Testing non-collectible field multiply:");
      AbstrTest i2 = (AbstrTest)Activator.CreateInstance(
        CreateTypeField("MyClassModule2", false));
      stopwatch = Stopwatch.StartNew();
      accu = 0;
      for (uint i = 0; i < 0xffffffff; i++)
        accu += i2.MyMethod(i);
      stopwatch.Stop();
      Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");

      Console.WriteLine("Testing collectible field multiply:");
      AbstrTest i3 = (AbstrTest)Activator.CreateInstance(
        CreateTypeField("MyClassModule3", true));
      stopwatch = Stopwatch.StartNew();
      accu = 0;
      for (uint i = 0; i < 0xffffffff; i++)
        accu += i3.MyMethod(i);
      stopwatch.Stop();
      Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");
    }
    catch (Exception e) {
      Console.WriteLine("Exception Caught " + e.Message);
    }
  }
}

所以我的问题是:为什么它变慢了?

【问题讨论】:

    标签: c# .net clr


    【解决方案1】:

    是的,这是静态变量分配方式的必然结果。我将首先描述您如何将“视觉”放回 Visual Studio,当您可以查看抖动生成的机器代码时,您只有在诊断此类性能问题时才有机会。

    这对于 Reflection.Emit 代码来说很棘手,您无法单步执行委托调用,也无法找到代码的确切生成位置。您要做的是注入对 Debugger.Break() 的调用,以便调试器在正确的位置停止。所以:

        ILGenerator methodIL = myMethodBuilder.GetILGenerator();
        var brk = typeof(Debugger).GetMethod("Break");
        methodIL.Emit(OpCodes.Call, brk);
        methodIL.Emit(OpCodes.Ldarg_1);
        // etc..
    

    将循环重复次数更改为 1。工具 > 选项 > 调试 > 常规。取消勾选“Just My Code”和“Suppress JIT optimization”。调试选项卡 > 勾选“启用本机代码调试”。切换到发布版本。我将发布 32 位代码,它更有趣,因为 x64 抖动可以做得更好。

    “Testing non-collectible field multiply”测试的机器代码如下:

    01410E70  push        dword ptr [ebp+0Ch]        ; Ldarg_1, high 32-bits
    01410E73  push        dword ptr [ebp+8]          ; Ldarg_1, low 32-bits
    01410E76  push        dword ptr ds:[13A6528h]    ; myFieldBuilder, high 32-bits
    01410E7C  push        dword ptr ds:[13A6524h]    ; myFieldBuilder, low 32-bits 
    01410E82  call        @JIT_LMul@16 (73AE1C20h)   ; 64 bit multiply
    

    没什么大不了的,它调用了一个 CLR 辅助方法来执行 64 位乘法。 x64 抖动可以通过一条 IMUL 指令来完成。注意对静态myFieldBuilder 变量的访问,它有一个硬编码地址,0x13A6524。在您的机器上会有所不同。这非常有效。

    现在令人失望的是:

    059F0480  push        dword ptr [ebp+0Ch]        ; Ldarg_1, high 32-bits
    059F0483  push        dword ptr [ebp+8]          ; Ldarg_1, low 32-bits
    059F0486  mov         ecx,59FC8A0h               ; arg2 = DynamicClassDomainId
    059F048B  xor         edx,edx                    ; arg1 = DomainId
    059F048D  call        JIT_GetSharedNonGCStaticBaseDynamicClass (73E0A6C7h)  
    059F0492  push        dword ptr [eax+8]          ; @myFieldBuilder, high 32-bits
    059F0495  push        dword ptr [eax+4]          ; @myFieldBuilder, low 32-bits
    059F0498  call        @JIT_LMul@16 (73AE1C20h)   ; 64-bit multiply
    

    你可以从半英里外看出为什么它变慢了,有一个额外的对 JIT_GetSharedNonGCStaticBaseDynamicClass 的调用。它是 CLR 中的一个辅助函数,专门用于处理使用 AssemblyBuilderAccess.RunAndCollect 构建的 Reflection.Emit 代码中使用的静态变量。你今天可以看到源代码,它is here。让每个人的眼睛都流血了,但它是将 AppDomain 标识符和动态类标识符(也称为类型句柄)映射到存储静态变量的已分配内存块的函数。

    在“不可收集”版本中,抖动知道存储静态变量的特定地址。它在从与 AppDomain 关联的称为“加载程序堆”的内部结构中提取代码时分配变量。知道了变量的准确地址,就可以直接发出机器码中变量的地址。当然非常有效,没有办法更快地做到这一点。

    但这不能在“可收集”版本中工作,它不仅要垃圾收集机器代码,而且静态变量。这只能在动态分配存储时起作用。所以可以动态释放。与字典相比,额外的间接性使代码变慢。

    您现在可能会理解为什么除非卸载 AppDomain 才能卸载 .NET 程序集(和代码)的原因。这是一个非常非常重要的性能优化。

    不确定您希望获得什么样的推荐。一种是自己处理静态变量存储,一个具有实例字段的类。收集这些没问题。仍然不会那么快,它需要额外的间接,但绝对比让 CLR 处理它要快。

    【讨论】:

    • 这就是我在上一段中提出的。不是静态的,类的实例字段,您将只有一个类对象的实例。您必须在 ILGenerator.Emit() 调用中使用它。并使用 GCHandle.Alloc() 确保它在代码被收集之前保持活动状态。
    • 我认为核心限制是在收集可收集程序集时,抖动不能安全地假设静态变量将被销毁或重置。让它保留对代码已消失的类对象的引用是灾难性的且可被利用。类似的东西。当然,“为什么”对解决您的问题没有帮助。
    • 好答案。非常感谢。
    猜你喜欢
    • 2020-08-12
    • 1970-01-01
    • 2017-03-03
    • 1970-01-01
    • 1970-01-01
    • 2020-11-14
    • 2021-03-11
    • 1970-01-01
    • 2011-12-26
    相关资源
    最近更新 更多