c# - ¿Por qué Calli es más rápido que una llamada de delegado?
.net reflection.emit (2)
Estaba jugando con Reflection.Emit y descubrí sobre el EmitCalli
poco usado. Intrigado, me pregunté si es diferente de una llamada de método normal, así que hice el código a continuación:
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);
}
Ejecuté el código en modo x86 y modo x64. ¿Los resultados?
32 bits:
- Versión del delegado: 994
- Versión de Calli: 46
64 bits:
- Versión del delegado: 326
- Versión de Calli: 83
Supongo que la pregunta es obvia a estas alturas ... ¿por qué hay una gran diferencia de velocidad?
Actualizar:
También creé una versión P / Invoke de 64 bits:
- Versión del delegado: 284
- Versión de Calli: 77
- Versión P / Invoke: 31
Aparentemente, P / Invoke es más rápido ... ¿es esto un problema con mi evaluación comparativa, o hay algo en juego que no entiendo? (Estoy en modo de liberación, por cierto.)
Dados sus números de desempeño, ¿asumo que debe usar el marco 2.0 o algo similar? Los números son mucho mejores en 4.0, pero la versión "Marshal.GetDelegate" es aún más lenta.
La cosa es que no todos los delegados son creados iguales.
Los delegados para las funciones de código administrado son esencialmente una simple llamada de función (en x86, eso es una __fastcall), con la adición de un pequeño "switcheroo" si está llamando a una función estática (pero eso es solo 3 o 4 instrucciones en x86).
Los delegados creados por "Marshal.GetDelegateForFunctionPointer", por otro lado, son una llamada de función directa a una función "stub", que hace un poco de sobrecarga (ordenamiento y otras cosas) antes de llamar a la función no administrada. En este caso, hay muy pocas referencias, y la asignación para esta llamada parece estar bastante optimizada en 4.0 (pero lo más probable es que todavía pase por el intérprete de ML en 2.0), pero incluso en 4.0, hay una pila que exige permisos de código no administrados que no es parte de su delegado calli.
En general, descubrí que, aparte de conocer a alguien en el equipo de desarrollo de .NET, lo mejor que puedes hacer para descubrir qué está pasando con una interoperabilidad administrada / no administrada es investigar un poco con WinDbg y SOS.
Difícil de responder :) De todos modos lo intentaré.
El EmitCalli es más rápido porque es una llamada de código de byte sin procesar. Sospecho que SuppressUnmanagedCodeSecurity también deshabilitará algunas comprobaciones, por ejemplo, la saturación de la pila / la matriz fuera de límites comprueba el índice. Así que el código no es seguro y corre a toda velocidad.
La versión del delegado tendrá algún código compilado para verificar la escritura y también hará una llamada de desreferencia (porque el delegado es como un puntero con función de tipo).
¡Mis dos centavos!