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

Почему лямбда быстрее, чем IL-инъекция динамического метода?

Я только что построил динамический метод - см. ниже (спасибо другим пользователям SO). Похоже, что Func создан как динамический метод с инъекцией IL 2x медленнее, чем лямбда.

Кто-нибудь знает, почему именно?

(EDIT: он был выпущен как версия x64 в VS2010. Запустите его с консоли не изнутри Visual Studio F5.)

class Program
{
    static void Main(string[] args)
    {
        var mul1 = IL_EmbedConst(5);
        var res = mul1(4);

        Console.WriteLine(res);

        var mul2 = EmbedConstFunc(5);
        res = mul2(4);

        Console.WriteLine(res);

        double d, acc = 0;

        Stopwatch sw = new Stopwatch();

        for (int k = 0; k < 10; k++)
        {
            long time1;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul2(i);
                acc += d;
            }

            sw.Stop();

            time1 = sw.ElapsedMilliseconds;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul1(i);
                acc += d;
            }

            sw.Stop();

            Console.WriteLine("{0,6} {1,6}", time1, sw.ElapsedMilliseconds);
        }

        Console.WriteLine("\n{0}...\n", acc);
        Console.ReadLine();
    }

    static Func<int, int> IL_EmbedConst(int b)
    {
        var method = new DynamicMethod("EmbedConst", typeof(int), new[] { typeof(int) } );

        var il = method.GetILGenerator();

        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldc_I4, b);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Ret);

        return (Func<int, int>)method.CreateDelegate(typeof(Func<int, int>));
    }

    static Func<int, int> EmbedConstFunc(int b)
    {
        return a => a * b;
    }
}

Вот вывод (для i7 920)

20
20

25     51
25     51
24     51
24     51
24     51
25     51
25     51
25     51
24     51
24     51

4.9999995E+15...

=============================================== =============================

ИЗМЕНИТЬ EDIT EDIT EDIT EDIT

Вот доказательство того, что dhtorpe был прав - более сложная лямбда потеряет свое преимущество. Код для доказательства (это демонстрирует, что Lambda имеет ровно ту же производительность при инъекции IL):

class Program
{
    static void Main(string[] args)
    {
        var mul1 = IL_EmbedConst(5);
        double res = mul1(4,6);

        Console.WriteLine(res);

        var mul2 = EmbedConstFunc(5);
        res = mul2(4,6);

        Console.WriteLine(res);

        double d, acc = 0;

        Stopwatch sw = new Stopwatch();

        for (int k = 0; k < 10; k++)
        {
            long time1;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul2(i, i+1);
                acc += d;
            }

            sw.Stop();

            time1 = sw.ElapsedMilliseconds;

            sw.Restart();

            for (int i = 0; i < 10000000; i++)
            {
                d = mul1(i, i + 1);
                acc += d;
            }

            sw.Stop();

            Console.WriteLine("{0,6} {1,6}", time1, sw.ElapsedMilliseconds);
        }

        Console.WriteLine("\n{0}...\n", acc);
        Console.ReadLine();
    }

    static Func<int, int, double> IL_EmbedConst(int b)
    {
        var method = new DynamicMethod("EmbedConstIL", typeof(double), new[] { typeof(int), typeof(int) });

        var log = typeof(Math).GetMethod("Log", new Type[] { typeof(double) });

        var il = method.GetILGenerator();

        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldc_I4, b);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Conv_R8);

        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, b);
        il.Emit(OpCodes.Mul);
        il.Emit(OpCodes.Conv_R8);

        il.Emit(OpCodes.Call, log);

        il.Emit(OpCodes.Sub);

        il.Emit(OpCodes.Ret);

        return (Func<int, int, double>)method.CreateDelegate(typeof(Func<int, int, double>));
    }

    static Func<int, int, double> EmbedConstFunc(int b)
    {
        return (a, z) => a * b - Math.Log(z * b);
    }
} 
4b9b3361

Ответ 1

Учитывая, что разница в производительности существует только при запуске в режиме освобождения без прикрепленного отладчика, единственное объяснение, о котором я могу думать, заключается в том, что компилятор JIT способен создавать собственные оптимизаторы кода для выражения лямбда, которое он не может выполнить для испускаемой динамической функции IL.

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

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

Единственное различие между этими двумя прогонами заключается в поведении JIT. Когда процесс отлаживается, компилятор JIT подавляет ряд оптимизаций генерации собственных кодов, чтобы сохранить собственную инструкцию IL для сопоставлений номеров исходных кодов и других корреляций, которые были бы разрушены с помощью агрессивных нативных инструкций.

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

Вполне возможно, что ваши инструкции IL точно не соответствуют шаблону, который заставляет компилятор JIT оптимизировать выражение лямбда. Например, ваши IL-команды кодируют значение B как встроенную константу, тогда как аналогичное лямбда-выражение загружает поле из внутреннего захваченного экземпляра объекта переменной. Даже если ваш сгенерированный IL должен имитировать захваченный шаблон поля компилятора С#, генерируемого lambda-выражением IL, он все равно может быть не "достаточно близок" для получения того же метода JIT, что и выражение лямбда.

Как упоминалось в комментариях, это может быть связано с вложением лямбда для устранения накладных расходов на возврат/возврат. Если это так, я бы ожидал увидеть, что это различие в производительности исчезает в более сложных лямбда-выражениях, поскольку вложение обычно зарезервировано только для самых простых выражений.

Ответ 2

Постоянная 5 была причиной. Почему это так? Причина: Когда JIT знает, что константа равна 5, она не генерирует инструкцию imul, а lea [rax, rax * 4]. Это хорошо известная оптимизация уровня сборки. Но по какой-то причине этот код выполнялся медленнее. Оптимизация была пессимизацией.

И компилятор С#, испускающий замыкание, не позволял JIT оптимизировать код таким образом.

Доказательство. Измените константу на 56878567 и измените производительность. При проверке кода JITed вы можете видеть, что imul используется сейчас.

Мне удалось поймать это путем жесткого кодирования константы 5 в лямбда следующим образом:

    static Func<int, int> EmbedConstFunc2(int b)
    {
        return a => a * 5;
    }

Это позволило мне проверить JITed x86.

Sidenote:.NET JIT никак не встраивает вызовы делегатов. Просто упомянул об этом, потому что это ложно показало, что это было в комментариях.

Sidenode 2: Чтобы получить полный уровень оптимизации JIT, вам необходимо скомпилировать его в режиме деблокирования и начать без приложения отладчика. Отладчик предотвращает выполнение оптимизации даже в режиме Release.

Sidenote 3: Хотя EmbedConstFunc содержит замыкание и обычно будет медленнее, чем динамически генерируемый метод, эффект этой "lea" -оптимизации наносит больше урона и в конечном итоге медленнее.

Ответ 3

lambda не быстрее DynamicMethod. Он основан на. Однако статический метод быстрее, чем метод экземпляра, но делегировать создание для статического метода медленнее, чем делегировать создание метода экземпляра. Выражение Lambda создает статический метод, но использует его как метод экземпляра, добавляя в качестве первого paameter значение "Closure". Делегировать в статический метод "pop" stack, чтобы избавиться от необязательного экземпляра "this" до "mov" до реального "тела IL". в случае делегирования, например, метод "IL body" напрямую попадает. Вот почему делегат гипотетического статического метода, построенного по выражению лямбда, является более быстрым (возможно, побочным эффектом совместного использования кода шаблона делегата между экземпляром/статическим методом)

Проблему производительности можно избежать, добавив неиспользуемый первый аргумент (например, тип Closure) в DynamicMethod и вызовите CreateDelegate с явным целевым экземпляром (нуль можно использовать).

var myDelegate = DynamicMethod.CreateDelegate(MyDelegateType, null) как MyDelegateType;

http://msdn.microsoft.com/fr-fr/library/z43fsh67 (v = vs .110).aspx

Тони THONG