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

Почему Calli быстрее, чем вызов делегата?

Я играл с Reflection.Emit и узнал о малоиспользуемом EmitCalli. Заинтригованный, я задавался вопросом, не отличается ли он от обычного вызова метода, поэтому я взломал код ниже:

using System;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using System.Security;

[SuppressUnmanagedCodeSecurity]
static class Program
{
    const long COUNT = 1 << 22;
    static readonly byte[] multiply = IntPtr.Size == sizeof(int) ?
      new byte[] { 0x8B, 0x44, 0x24, 0x04, 0x0F, 0xAF, 0x44, 0x24, 0x08, 0xC3 }
    : new byte[] { 0x0f, 0xaf, 0xca, 0x8b, 0xc1, 0xc3 };

    static void Main()
    {
        var handle = GCHandle.Alloc(multiply, GCHandleType.Pinned);
        try
        {
            //Make the native method executable
            uint old;
            VirtualProtect(handle.AddrOfPinnedObject(),
                (IntPtr)multiply.Length, 0x40, out old);
            var mulDelegate = (BinaryOp)Marshal.GetDelegateForFunctionPointer(
                handle.AddrOfPinnedObject(), typeof(BinaryOp));

            var T = typeof(uint); //To avoid redundant typing

            //Generate the method
            var method = new DynamicMethod("Mul", T,
                new Type[] { T, T }, T.Module);
            var gen = method.GetILGenerator();
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Ldc_I8, (long)handle.AddrOfPinnedObject());
            gen.Emit(OpCodes.Conv_I);
            gen.EmitCalli(OpCodes.Calli, CallingConvention.StdCall,
                T, new Type[] { T, T });
            gen.Emit(OpCodes.Ret);

            var mulCalli = (BinaryOp)method.CreateDelegate(typeof(BinaryOp));

            var sw = Stopwatch.StartNew();
            for (int i = 0; i < COUNT; i++) { mulDelegate(2, 3); }
            Console.WriteLine("Delegate: {0:N0}", sw.ElapsedMilliseconds);
            sw.Reset();

            sw.Start();
            for (int i = 0; i < COUNT; i++) { mulCalli(2, 3); }
            Console.WriteLine("Calli:    {0:N0}", sw.ElapsedMilliseconds);
        }
        finally { handle.Free(); }
    }

    delegate uint BinaryOp(uint a, uint b);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool VirtualProtect(
        IntPtr address, IntPtr size, uint protect, out uint oldProtect);
}

Я запускал код в режиме x86 и режиме x64. Результаты?

32-бит:

  • Версия делегата: 994
  • Версия для каллиграфии: 46

64-бит:

  • Версия делегата: 326
  • Версия для Calli: 83

Я думаю, что теперь вопрос очевиден... почему существует такая огромная разница в скорости?


Update:

Я создал 64-битную версию P/Invoke:

  • Версия делегата: 284
  • Calli version: 77
  • Версия P/Invoke: 31

По-видимому, P/Invoke быстрее... это проблема с моим бенчмаркингом или что-то происходит, я не понимаю? (Кстати, я в режиме выпуска).

4b9b3361

Ответ 1

Учитывая ваши показатели производительности, я предполагаю, что вы должны использовать фреймворк 2.0 или что-то подобное? Числа намного лучше в 4.0, но версия "Marshal.GetDelegate" все еще медленнее.

Дело в том, что не все делегаты созданы равными.

Делегированные функции управляемых кодов - это просто прямой вызов функции (на x86, который __fastcall), с добавлением небольшого "switcheroo", если вы вызываете статическую функцию (но это всего лишь 3 или 4 инструкции на x86).

Делегаты, созданные с помощью "Marshal.GetDelegateForFunctionPointer", с другой стороны, являются прямым вызовом функции в функцию "заглушка", которая делает небольшую накладную (сортировку и многое другое) перед вызовом неуправляемой функции. В этом случае очень мало маршаллинга, и сортировка для этого вызова, по-видимому, в значительной степени оптимизирована в 4.0 (но, скорее всего, все еще проходит через интерпретатор ML на 2.0), но даже в 4.0 существует stackWalk, требующий неуправляемых кодовых разрешений, которые не является частью вашего делегата calli.

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

Ответ 2

Трудно ответить:) В любом случае я попробую.

EmitCalli быстрее, потому что он является сырым байтовым кодом. Я подозреваю, что SuppressUnmanagedCodeSecurity также отключит некоторые проверки, например стекирование переполнения/массива из проверок индексов. Таким образом, код не безопасен и работает на полной скорости.

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

Мои два цента!