Встраивание метода ILGenerator

Учитывая следующий код:

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

namespace ConsoleApplication1
{
    class A
    {
        public int Do(int n)
        {
            return n;
        }
    }

    public delegate int DoDelegate();

    class Program
    {
        public static void Main(string[] args)
        {
            A a = new A();

            Stopwatch stopwatch = Stopwatch.StartNew();
            int s = 0;
            for (int i = 0; i < 100000000; i++)
            {
                s += a.Do(i);
            }

            Console.WriteLine(stopwatch.ElapsedMilliseconds);
            Console.WriteLine(s);


            DynamicMethod dm = new DynamicMethod("Echo", typeof(int), new Type[] { typeof(int) }, true);
            ILGenerator il = dm.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ret);

            DynamicMethod dm2 = new DynamicMethod("Test", typeof(int), new Type[0]);
            il = dm2.GetILGenerator();


            Label loopStart = il.DefineLabel();
            Label loopCond = il.DefineLabel();

            il.DeclareLocal(typeof(int));   // i
            il.DeclareLocal(typeof(int));   // s

            // s = 0;
            il.Emit(OpCodes.Ldc_I4_0);
            il.Emit(OpCodes.Stloc_1);

            // i = 0;
            il.Emit(OpCodes.Ldc_I4_0);
            il.Emit(OpCodes.Stloc_0);

            il.Emit(OpCodes.Br_S, loopCond);

            il.MarkLabel(loopStart);

            // s += Echo(i);
            il.Emit(OpCodes.Ldloc_1);   // Load s
            il.Emit(OpCodes.Ldloc_0);   // Load i
            il.Emit(OpCodes.Call, dm);  // Call echo method
            il.Emit(OpCodes.Add);
            il.Emit(OpCodes.Stloc_1);

            // i++
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ldc_I4_1);
            il.Emit(OpCodes.Add);
            il.Emit(OpCodes.Stloc_0);

            il.MarkLabel(loopCond);

            // Check for loop condition
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ldc_I4, 100000000);
            il.Emit(OpCodes.Blt_S, loopStart);

            il.Emit(OpCodes.Ldloc_1);
            il.Emit(OpCodes.Ret);


            DoDelegate doDel = (DoDelegate)dm2.CreateDelegate(typeof(DoDelegate));
            s = doDel.Invoke();     // Dummy run to force JIT


            stopwatch = Stopwatch.StartNew();
            s = doDel.Invoke();
            Console.WriteLine(stopwatch.ElapsedMilliseconds);
            Console.WriteLine(s);
        }
    }
}

Вызов метода Do становится встроенным. Цикл завершается примерно через 40 мс. Если я, например, сделаю Do виртуальной функцией, она не будет встроена, и цикл завершится через 240 мс. Все идет нормально. Когда я использую ILGenerator для создания метода Do (Echo), а затем генерирую DynamicMethod с тем же циклом, что и данный основной метод, вызов метода Echo никогда не встраивается, и для завершения цикла требуется около 240 мс. Код MSIL правильный, так как он возвращает тот же результат, что и код C#. Я был уверен, что встраивание методов — это то, что делает JIT, поэтому я не вижу причин не встраивать метод Echo.

Кто-нибудь знает, почему этот простой метод не будет встроен JIT.


person user102808    schedule 31.12.2011    source источник
comment
Вы также генерируете код, который вызывает динамически сгенерированный метод Do(), или этот код известен во время компиляции?   -  person Tim Medora    schedule 31.12.2011
comment
Не могли бы вы включить полный пример кода, в котором используется ILGenerator? И, просто чтобы быть уверенным: вы тестируете релизную сборку без подключенного отладчика?   -  person Pieter van Ginkel    schedule 31.12.2011
comment
Я отредактировал сообщение, предоставив полный код тестового приложения. Я использую релизную сборку и запускаю ее без отладчика. Цикл for C# встраивает вызов метода и работает значительно быстрее, чем цикл for IL.   -  person user102808    schedule 31.12.2011


Ответы (3)


После дальнейшего расследования я пришел к следующему выводу:

  1. Методы, сгенерированные ILGenerator, никогда не будут встроены. Неважно, вызываете ли вы их с помощью делегата, из другого DynamicMethod или из метода, созданного с помощью MethodBuilder.
  2. Существующие методы (закодированные на C# и скомпилированные VS) могут быть встроены только при вызове из метода, созданного с помощью MethodBuilder. При вызове из DynamicMethod они никогда не будут встроены.

Я пришел к такому выводу после тщательного тестирования многих образцов и изучения окончательного кода на ассемблере.

person user102808    schedule 31.12.2011
comment
Пока кто-то не покажет иначе, думаю, это верный вывод. Иногда имеет смысл не встраивать динамические методы (что я и предложил в своем ответе). Возможно, разработчики компилятора решили, что проще всего считать это правилом для всех случаев. Интересно, ведут ли себя деревья выражений одинаково: msdn.microsoft.com/en- нас/библиотека/bb397951.aspx - person Tim Medora; 01.01.2012
comment
Было бы интересно проверить, хотя у меня не было времени (да и необходимости) играть с деревьями выражений. - person user102808; 01.01.2012
comment
Конечно, это также включает IL-код, сгенерированный и сохраненный как dll и загруженный после сохранения? Потому что я уверен, что он не включает сгенерированные сборки, которые были сохранены. В любом случае, если ваш ответ правильный, вы можете просто отметить его как правильный. - person Felix K.; 27.02.2012

Если я правильно понимаю, я предполагаю, что, поскольку метод генерируется динамически, компилятор JIT не знает, как встроить его для вызывающих.

Я написал много IL, но я не изучал поведение встраивания (главным образом потому, что динамические методы обычно достаточно быстры для моих целей без дальнейшей оптимизации).

Я бы хотел, чтобы кто-нибудь, более осведомленный в этом вопросе, оставил отзыв (пожалуйста, не просто голосуйте против; я хотел бы узнать что-то здесь, если я ошибаюсь).

Нединамический

  • вы пишете «обычный» код .NET (например, C#, VB.NET, любой язык, поддерживающий CLS)
  • IL создается во время компиляции
  • машинный код создается во время выполнения; методы встроены там, где это необходимо

Динамический

  • вы пишете «обычный» код .NET, целью которого является создание динамического метода
  • IL создается во время компиляции для этого кода, но динамический метод не создается
  • машинный код создается во время выполнения для кода, который генерирует динамический метод
  • когда этот код вызывается, динамический метод создается как IL в специальной сборке.
  • IL динамического метода компилируется в машинный код
  • однако JIT-компилятор не перекомпилирует другие вызывающие объекты для встраивания нового динамического метода. Или, возможно, другие вызывающие объекты сами по себе являются динамическими и еще не созданы.
person Tim Medora    schedule 31.12.2011
comment
Я добавил полный пример кода, который воспроизводит проблему. Как видите, я генерирую два динамических метода. Один чрезвычайно прост и просто возвращает то же значение, что и параметр. Другой метод запускает простой цикл for (тот же самый, что и в коде C# в начале). Я не ожидаю, что JIT встроит вызов второго метода, потому что он вызывается через делегат. Однако я ожидаю, что он встроит первый метод, когда он вызывается внутри второго метода. Когда я сам встраиваю его, я получаю в 10 раз более быстрое выполнение того же цикла. - person user102808; 31.12.2011

Вы можете попробовать создать динамическую сборку. Насколько я понимаю, большая часть среды выполнения не знает, что она динамическая. Я думаю, что внутри он загружается, как и любая другая сборка byte[]. Поэтому JIT/инлайнер также может не знать об этом (или ему все равно).

person usr    schedule 31.12.2011
comment
Я даже пробовал это. Каким-то образом и по какой-то причине динамически сгенерированный метод (независимо от того, как вы его генерируете) не будет встроен. Спасибо, в любом случае. - person user102808; 01.01.2012