可收集的动态装配体中的静态字段访问缺乏性能

本文关键字:访问 字段 性能 静态 动态 可收集 | 更新日期: 2023-09-27 18:35:03

对于动态二进制转换模拟器,我需要使用访问静态字段的类生成可收集的 .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);
    }
  }
}

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

可收集的动态装配体中的静态字段访问缺乏性能

是的,这是静态变量分配方式的一个非常不可避免的结果。 我将首先描述如何将"视觉"放回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。 工具>选项>调试>常规。 取消选中"仅我的代码"和"禁止 JIT 优化"。 "调试"选项卡>勾选"启用本机代码调试"。 切换到发布版本。我将发布 32 位代码,它更有趣,因为 x64 抖动可以做得更好。

"测试不可收集字段乘法"测试的机器代码如下所示:

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.RunAndCollection 构建的 Reflection.Emit 代码中使用的静态变量。 你今天可以看到源头,它在这里。 让每个人都流血,但它是将 AppDomain 标识符和动态类标识符(也称为类型句柄(映射到存储静态变量的已分配内存块的函数。

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

但这在"可收集"版本中不起作用,它不仅要垃圾收集机器代码,还要垃圾收集静态变量。 这只有在动态分配存储时才能工作。 因此,它可以动态发布。 与字典相比,额外的间接寻址使代码变慢。

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

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