2010-01-17 23 views
49

Después de esta pregunta - Pass Method as Parameter using C# y algo de mi experiencia personal, me gustaría saber un poco más sobre el rendimiento de llamar a un delegado frente a simplemente llamar a un método en C#.Rendimiento de llamar a los delegados vs métodos

Aunque los delegados son extremadamente convenientes, tenía una aplicación que realizaba muchas devoluciones de llamadas a través de delegados y cuando volvimos a escribir esto para usar interfaces de devolución de llamada conseguimos 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 las llamadas a delegados internamente en el compilador/CLR y cómo afecta esto el rendimiento de las llamadas a métodos?


EDITAR - Para aclarar lo que quiero decir con delegados vs interfaces de devolución de llamada.

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

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

+1

no estoy claro en lo que estás pidiendo ... devoluciones de llamada de interfaces _are_ delegados. –

+1

Ver http://stackoverflow.com/questions/1269452/is-using-delegates-excessively-a-bad-idea-for-performance y http://stackoverflow.com/questions/304770/does-using-delegates- slow-down-my-net-programs - posibles duplicados? El delegado – itowlson

+0

es necesario si está ejecutando subprocesos separados y necesita interactuar con el subproceso de interfaz de usuario ... por lo que debe refinar su pregunta para que sea más localizada y menos genérica. –

Respuesta

66

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

Aquí es un punto de referencia muy áspera y lista, que muestra (en mi caja de todos modos) delegados estar realmente más rápido 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<int, int> 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 sobre el misma):

Interface: 5068 
Delegate: 4404 

Ahora no tienen una particular fe que eso significa que los delegados son realmente más rápido que las interfaces ... pero me hace bastante convencido de que no son una orden o f magnitud más lenta. 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 reutilizarlo. Por ejemplo:

Func<int, int> 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); 
} 

Podría haber sido el problema que estaban viendo?

+1

¿Puede explicar lo que hace el compilador en el último caso? ¿Crea una nueva instancia de delegado en cada iteración o? – Jan

+2

¿Cambiaría esto si lo convirtiera en un evento usando el delegado? –

+0

@JanJ: compruebe el código compilado con ildasm, pero creo que sí, sí. @Chris S: ¿Podrías dar más detalles sobre lo que quieres decir? –

19

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 los métodos de interfaz.

Ver el blog de Joel Pobar.

+0

Gracias, hay información útil allí. – Paolo

15

Me resulta 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 delegados se implementan generalmente algo como (usando la notación de estilo C, pero por favor, perdona los errores de sintaxis de menor importancia ya que esto es sólo un ejemplo):

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

Llamar a un delegado funciona algo así como:

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

una clase, traducido a C, sería algo así como:

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

llamar a una función vritual, tendría que hacer 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 las mismas, 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 rama de CPU adivinarán la dirección del puntero de función y ejecutarán especulativamente su objetivo 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 .

+1

Esa fue siempre mi suposición hasta que encontré la anomalía que describí en la pregunta. Tal vez como Jon sugiere que algo más fue el problema y me he quedado atrapado en un meme de "delegados son más lentos" por error. – Paolo

1

¿Qué pasa con el hecho de que los delegados 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?

+0

Conceptualmente, no hay ninguna razón por la cual el soporte para delegados de multidifusión tendría que ralentizar la invocación en el caso de objetivo único. Si los delegados con objetivos múltiples configuran su puntero de método interno a un método ExecuteMultiDelegate especial, y su referencia de destino interno a una matriz de pares de estructuras (Objeto, Método), los delegados podrían despachar sin condiciones a su método sin verificar si había múltiples objetivos. El método ExecuteMultiDelegate tendría que tener algo del comportamiento normal de verificación de tipo deshabilitado, pero eso debería ser factible. – supercat

+0

Tenga en cuenta que el enfoque que acabo de describir no es AFAIK acerca de cómo se implementan realmente MulticastDelegates, pero sería un mecanismo para optimizar el caso más común (exactamente un objetivo). – supercat

4

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.

Considerando que la variable ya está en el tipo correcto (interfaz o delegado) e invocación 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.

Éstos son los resultados:

para obtener resultados reales, compilar este en modo de lanzamiento y ejecutarlo fuera de Visual Studio.

Comprobar llamadas directas doble
00: 00: 00,5834988
00: 00: 00,5997071

Comprobar llamadas de interfaz, obteniendo la interfaz en cada llamada
00: 00: 05,8998212

Comprobación llamadas a la interfaz, obteniendo la interfaz una vez
00:00:05.3163224

Comprobación de Acción (delegado) llama, conseguir la acción en cada llamada
00: 00: 17.1807980

Comprobación de Acción (delegado) llama, conseguir la acción una vez
00: 00: 05,3163224

Comprobación de Acción (delegado) sobre un método de interfaz, tanto en conseguir cada llamada
00: 03: 50,7326056

Comprobación de Acción (delegado) a través de una método de interfaz n, consiguiendo la interfaz vez, el delegado en cada llamada
00: 03: 48,9141438

Comprobación de Acción (delegado) sobre un método de interfaz, consiguiendo 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 llamar es muy rápido. Pero tener que conseguir un delegado es más lento que tener que obtener una interfaz. Tener que obtener un delegado sobre 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 ha generado estos 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(); 
     } 
    } 

} 
+2

Probablemente no deba incluir obtener al delegado en el tiempo que lleva ejecutarlo. – TamusJRoyce

+5

También debe usar la [Clase de cronómetro] (http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch (v = vs.110) .aspx) en lugar de DateTime. Eric Lippert entra en [más detalles] (http://tech.pro/tutorial/1295/c-performance-benchmark-mistakes-part-two). – FriendlyGuy

Cuestiones relacionadas