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

Несколько типов в одной динамической сборке являются более медленными, чем несколько динамических сборок с одним типом

Итак, я испускаю динамические прокси через 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();
}
4b9b3361

Ответ 1

На моем ПК в LINQPad с использованием С# 7.0 я получаю одну сборку около 8,8 секунд, по одной сборке на тип около 2,6 секунды. Большая часть времени в одной сборке находится в DefineType и CreateType, тогда как во время в основном находится DefineDynamicAssembly + DefineDynamicModule.

DefineType проверяет, нет конфликтов имен, это поиск Dictionary. Если Dictionary пуст, это означает проверку для null.

Большая часть времени проведена в CreateType, но я не вижу, где, однако кажется, что для добавления одного дополнительного модуля требуется дополнительное время добавления типов.

Создание нескольких модулей замедляет весь процесс, но большую часть времени тратится на создание модулей и в DefineType, которые должны сканировать каждый модуль для дубликата, так что теперь оно увеличилось до 10000 null проверок. С уникальным модулем для каждого типа CreateType выполняется очень быстро.

Ответ 2

В моих проверках, почему определение нескольких модулей в одной сборке происходит медленнее, чем создание новой сборки с одним модулем, используя эти фрагменты кода:

Сценарий с одной сборкой:

        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");
        }
  • Я обнаружил, что около 20% (например, 50% из нескольких сборок) времени, базовый код проходит через все имена модулей, чтобы проверить конфликт. Эта часть понятна и ожидаема.
  • При использовании одной сборки еще 60% -80% времени CLI DefineDynamicModule() находится под давлением. Однако при использовании нескольких сборок этот метод никогда не вызывается; вместо этого другие методы несут ответственность за оставшиеся 50%.

Давайте углубимся в документацию ECMA-335 для CLI.

II.6 Сборка представляет собой набор из одного или нескольких файлов, развернутых как единое целое.

Page 140

Итак, теперь мы понимаем, что сборка - это по существу пакет, а модули - основные компоненты. Это сказано:

II.6 Модуль - это один файл, содержащий исполняемый контент в указанном здесь формате. Если модуль содержит манифест, он также указывает модули (включая себя), которые составляют сборку. Узел должен содержать только один манифест среди всех его составных файлов.

Page 140

Основываясь на этой информации, мы знаем, что при создании сборки мы автоматически добавляем один модуль в сборку. Вот почему мы никогда не получаем удар по функции CLI DefineDynamicModule(), если мы продолжаем создавать новые сборки. Вместо этого мы получаем запрос на метод CLI GetInMemoryAssemblyModule() для получения информации о Манифест-модуле (модуль, который создается автоматически).

Итак, у нас есть небольшое увеличение производительности; с одной сборкой мы получаем 10001 модулей, но с несколькими сборками мы получаем в общей сложности 10000 модулей. Не так много, поэтому этот дополнительный модуль не должен быть основной причиной этого.

II.6.5 Когда элемент находится в текущей сборке, но является частью модуля, отличного от того, который содержит манифест, определяющий модуль должен быть объявлен в манифесте сборки, используя .module extern.

Page 146

и

II.6.7 Модуль манифеста, из которого может быть только один на сборку, включает директиву .assembly. Для экспорта типа, определенного в любом другом модуле сборки, требуется запись в манифесте сборки.

Page 146

Поэтому каждый раз, когда вы создаете новый модуль, вы фактически добавляете новый файл в архив, а затем модифицируете первый файл архива для ссылки на новый модуль. По существу в коде с одной сборкой мы добавляем 10000 модулей, а затем редактируем первый модуль 10000 раз. Это не относится к многоассемблерному коду, в котором мы редактируем только первый автоматически созданный модуль, 10000 раз.

Это накладные расходы, которые мы видим. И он растет экспоненциально в моей системе.

(5000 = 1,5 с, 10000 = 6 с, 20000 = 25 с)

С вашим кодом, однако, узким местом является неуправляемая функция CLR SetMethodIL, вызванная методом CreateTypeNoLock.CreateTypeNoLock(), и я еще ничего не нашел в документации об этом.

К сожалению, сложно декомпилировать и понять CLR.dll, чтобы увидеть, что на самом деле происходит там, и в результате мы просто делаем догадки на основе опубликованной на публике информации Microsoft на данном этапе.