Rendimiento de llamar a delegates vs métodos

Después de esta pregunta: Pase el Método como parámetro usando C # y parte de mi experiencia personal. Me gustaría saber un poco más sobre el rendimiento de llamar a un delegado y solo llamar a un método en C #.

Aunque los delegates son extremadamente convenientes, tuve una aplicación que hizo muchas devoluciones de llamadas a través de delegates y cuando reescribimos esto para usar interfaces de callback obtuvimos una mejora de velocidad de orden de magnitud. Esto fue con .NET 2.0, así que no estoy seguro de cómo han cambiado las cosas con 3 y 4.

¿Cómo se manejan internamente las llamadas a los delegates en el comstackdor / CLR y cómo afecta esto el rendimiento de las llamadas a métodos?


EDITAR – Para aclarar lo que quiero decir con delegates vs interfaces de callback.

Para llamadas asincrónicas, mi clase podría proporcionar un evento OnComplete y un delegado asociado al que la persona que llama podría suscribirse.

Alternativamente, podría crear una interfaz ICallback con un método OnComplete que la persona que llama implemente y luego se registre con la clase que luego llamará a ese método una vez finalizado (es decir, la forma en que Java maneja estas cosas).

No he visto ese efecto. Ciertamente nunca lo he visto como un cuello de botella.

Aquí hay un punto de referencia muy difícil y listo que muestra (en mi caja de todos modos) que los delegates realmente son más rápidos que las interfaces:

using System; using System.Diagnostics; interface IFoo { int Foo(int x); } class Program : IFoo { const int Iterations = 1000000000; public int Foo(int x) { return x * 3; } static void Main(string[] args) { int x = 3; IFoo ifoo = new Program(); Func del = ifoo.Foo; // Make sure everything's JITted: ifoo.Foo(3); del(3); Stopwatch sw = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { x = ifoo.Foo(x); } sw.Stop(); Console.WriteLine("Interface: {0}", sw.ElapsedMilliseconds); x = 3; sw = Stopwatch.StartNew(); for (int i = 0; i < Iterations; i++) { x = del(x); } sw.Stop(); Console.WriteLine("Delegate: {0}", sw.ElapsedMilliseconds); } } 

Resultados (.NET 3.5; .NET 4.0b2 es casi lo mismo):

 Interface: 5068 Delegate: 4404 

Ahora no tengo una fe particular en que eso signifique que los delegates son realmente más rápidos que las interfaces ... pero me hace bastante convencido de que no son un orden de magnitud más lentos. Además, esto no hace casi nada dentro del método delegado / interfaz. Obviamente, el costo de invocación va a hacer cada vez menos diferencia a medida que realiza más y más trabajo por llamada.

Una cosa a tener en cuenta es que no está creando un nuevo delegado varias veces donde solo usaría una sola instancia de interfaz. Esto podría causar un problema ya que provocaría la recolección de basura, etc. Si está utilizando un método de instancia como delegado dentro de un bucle, le resultará más eficiente declarar la variable de delegado fuera del bucle, crear una sola instancia de delegado y reutilizar eso. Por ejemplo:

 Func del = myInstance.MyMethod; for (int i = 0; i < 100000; i++) { MethodTakingFunc(del); } 

es más eficiente que:

 for (int i = 0; i < 100000; i++) { MethodTakingFunc(myInstance.MyMethod); } 

¿Pudo haber sido este el problema que estabas viendo?

Desde CLR v 2, el costo de la invocación de delegado es muy similar al de la invocación de método virtual, que se utiliza para métodos de interfaz.

Ver el blog de Joel Pobar .

Encuentro completamente inverosímil que un delegado sea sustancialmente más rápido o más lento que un método virtual. En todo caso, el delegado debería ser más rápido. En un nivel inferior, los delegates suelen implementar algo así como (usando la notación estilo C, pero perdonen cualquier error de syntax menor ya que esto es solo una ilustración):

 struct Delegate { void* contextPointer; // What class instance does this reference? void* functionPointer; // What method does this reference? } 

Llamar a un delegado funciona de la siguiente manera:

 struct Delegate myDelegate = somethingThatReturnsDelegate(); // Call the delegate in de-sugared C-style notation. ReturnType returnValue = (*((FunctionType) *myDelegate.functionPointer))(myDelegate.contextPointer); 

Una clase, traducida a C, sería algo así como:

 struct SomeClass { void** vtable; // Array of pointers to functions. SomeType someMember; // Member variables. } 

Para llamar a una función vritual, haría lo siguiente:

 struct SomeClass *myClass = someFunctionThatReturnsMyClassPointer(); // Call the virtual function residing in the second slot of the vtable. void* funcPtr = (myClass -> vtbl)[1]; ReturnType returnValue = (*((FunctionType) funcPtr))(myClass); 

Básicamente son los mismos, excepto que cuando se usan funciones virtuales se pasa por una capa adicional de direccionamiento indirecto para obtener el puntero de la función. Sin embargo, esta capa indirecta adicional es a menudo gratuita porque los predictores modernos de twig de CPU adivinarán la dirección del puntero de función y ejecutarán especulativamente su objective en paralelo con la búsqueda de la dirección de la función. He encontrado (aunque en D, no en C #) que las llamadas a funciones virtuales en un ciclo cerrado no son más lentas que las llamadas directas sin línea, con la condición de que para cualquier ejecución determinada del ciclo siempre se resuelvan a la misma función real .

Hice algunas pruebas (en .Net 3.5 … más tarde comprobaré en casa usando .Net 4). El hecho es: obtener un objeto como interfaz y luego ejecutar el método es más rápido que obtener un delegado de un método y luego llamar al delegado.

Teniendo en cuenta que la variable ya está en el tipo correcto (interfaz o delegado) y que la invocación es simple, hace que el delegado gane.

Por alguna razón, obtener un delegado sobre un método de interfaz (quizás sobre cualquier método virtual) es MUCHO más lento.

Y, considerando que hay casos en los que simplemente no podemos pre-almacenar el delegado (como en Despachos, por ejemplo), eso puede justificar por qué las interfaces son más rápidas.

Aquí están los resultados:

Para obtener resultados reales, compile esto en modo Release y ejecútelo fuera de Visual Studio.

Comprobando llamadas directas dos veces
00: 00: 00.5834988
00: 00: 00.5997071

Comprobando llamadas de interfaz, obteniendo la interfaz en cada llamada
00: 00: 05.8998212

Comprobando llamadas de interfaz, obteniendo la interfaz una vez
00: 00: 05.3163224

Verificación de llamadas de acción (delegar), obtener la acción en cada llamada
00: 00: 17.1807980

Verificación de llamadas de Acción (delegar), obteniendo la Acción una vez
00: 00: 05.3163224

Comprobando Acción (delegar) sobre un método de interfaz, obteniendo ambos en cada llamada
00: 03: 50.7326056

Comprobando Acción (delegar) sobre un método de interfaz, obteniendo la interfaz una vez, el delegado en cada llamada
00: 03: 48.9141438

Comprobando Acción (delegar) sobre un método de interfaz, obteniendo ambos una vez
00: 00: 04.0036530

Como puede ver, las llamadas directas son realmente rápidas. Almacenar la interfaz o delegar antes, y luego solo llamarla es realmente rápido. Pero tener que conseguir un delegado es más lento que tener que obtener una interfaz. Tener que delegar en un método de interfaz (o método virtual, no estoy seguro) es realmente lento (compare los 5 segundos de obtener un objeto como interfaz con los casi 4 minutos de hacer lo mismo para obtener la acción).

El código que generó esos resultados está aquí:

 using System; namespace ActionVersusInterface { public interface IRunnable { void Run(); } public sealed class Runnable: IRunnable { public void Run() { } } class Program { private const int COUNT = 1700000000; static void Main(string[] args) { var r = new Runnable(); Console.WriteLine("To get real results, compile this in Release mode and"); Console.WriteLine("run it outside Visual Studio."); Console.WriteLine(); Console.WriteLine("Checking direct calls twice"); { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { r.Run(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { r.Run(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking interface calls, getting the interface at every call"); { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { IRunnable interf = r; interf.Run(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking interface calls, getting the interface once"); { DateTime begin = DateTime.Now; IRunnable interf = r; for (int i = 0; i < COUNT; i++) { interf.Run(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) calls, getting the action at every call"); { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { Action a = r.Run; a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) calls, getting the Action once"); { DateTime begin = DateTime.Now; Action a = r.Run; for (int i = 0; i < COUNT; i++) { a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) over an interface method, getting both at every call"); { DateTime begin = DateTime.Now; for (int i = 0; i < COUNT; i++) { IRunnable interf = r; Action a = interf.Run; a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) over an interface method, getting the interface once, the delegate at every call"); { DateTime begin = DateTime.Now; IRunnable interf = r; for (int i = 0; i < COUNT; i++) { Action a = interf.Run; a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.WriteLine(); Console.WriteLine("Checking Action (delegate) over an interface method, getting both once"); { DateTime begin = DateTime.Now; IRunnable interf = r; Action a = interf.Run; for (int i = 0; i < COUNT; i++) { a(); } DateTime end = DateTime.Now; Console.WriteLine(end - begin); } Console.ReadLine(); } } } 

¿Qué pasa con el hecho de que los delegates son contenedores? ¿La capacidad de multidifusión no agrega sobrecarga? Mientras estamos en el tema, ¿qué pasa si empujamos este aspecto contenedor un poco más? Nada nos prohíbe, si d es un delegado, ejecutar d + = d; o desde la construcción de un gráfico dirigido arbitrariamente complejo de pares (puntero de contexto, puntero de método). ¿Dónde puedo encontrar la documentación que describe cómo se atraviesa este gráfico cuando se llama al delegado?