Подтвердить что ты не робот

Доступ к статическим полям в коллективных динамических узлах не имеет производительности

Для динамического бинарного симулятора перевода мне нужно собрать сборные сборки .NET с классами, которые обращаются к статическим полям. Однако при использовании статических полей внутри коллекционных сборок производительность исполнения в 2-3 раза ниже по сравнению с не коллекционными сборками. Это явление отсутствует в коллекционные сборки, которые не используют статические поля.

В приведенном ниже коде метод MyMethod абстрактного класса AbstrTest реализуется коллекционными и не коллективными динамическими сборками. Используя CreateTypeConst, MyMethod умножает значение аргумента ulong на постоянное значение в два, а при использовании 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);
    }
  }
}

Итак, мой вопрос: почему он медленнее?

4b9b3361

Ответ 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. Инструменты > Параметры > Отладкa > Общие. Отключить "Только мой код" и "Подавить оптимизацию JIT". Вкладка "Отладка" > "Включить отладку собственного кода". Переключитесь на сборку Release. Я отправлю 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        @[email protected] (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        @[email protected] (73AE1C20h)   ; 64-bit multiply

Вы можете сказать, почему это медленнее с расстояния в полмили, есть дополнительный вызов JIT_GetSharedNonGCStaticBaseDynamicClass. Это вспомогательная функция внутри CLR, специально разработанная для обработки статических переменных, используемых в Reflection.Emit, который был создан с помощью AssemblyBuilderAccess.RunAndCollect. Сегодня вы можете увидеть источник, здесь. Делает все глаза кровоточащими, но это функция, которая сопоставляет идентификатор AppDomain и динамический идентификатор класса (как дескриптор типа) для выделенной части памяти, в которой хранятся статические переменные.

В "не коллекционируемой" версии джиттер знает конкретный адрес, в котором хранится статическая переменная. Он присвоил переменную, когда она закодировала код из внутренней структуры, называемой "кучей загрузчика", связанной с AppDomain. Зная точный адрес переменной, он может напрямую испускать адрес переменной в машинный код. Разумеется, очень эффективно, нет возможности сделать это быстрее.

Но это не может работать в "коллекционной" версии, а просто не нужно мусор собирать машинный код, а также статические переменные. Это может работать только тогда, когда хранилище распределено динамически. Таким образом, он может быть динамически выпущен. Дополнительная косвенность, сравнивая ее с Словарем, делает код медленнее.

Теперь вы, возможно, оцените, почему сборки .NET(и код) не могут быть выгружены, если AppDomain не выгружен. Это очень важная перфомансия.

Не знаете, какую рекомендацию вы хотели бы получить. Можно было бы позаботиться о статическом хранилище переменных самостоятельно, классе с полями экземпляра. Нет проблем с получением собранных. Все еще не так быстро, это требует дополнительной косвенности, но определенно быстрее, чем позволить CLR позаботиться об этом.