2008-10-19 16 views
13

Me acaba de ocurrir una cuestión de diseño de código. Diga, tengo un método de "plantilla" que invoca algunas funciones que pueden "alterar". Un diseño intuitivo es seguir "Patrón de diseño de plantilla". Defina que las funciones de alteración sean funciones "virtuales" que se anularán en subclases. O bien, puedo usar funciones de delegado sin "virtual". Las funciones delegadas se inyectan para que también se puedan personalizar.C#: ¿La invocación de funciones virtuales es incluso más rápida que una invocación de delegado?

Originalmente, pensé que el segundo modo de "delegado" sería más rápido que el modo "virtual", pero algunos fragmentos de código prueban que no es correcto.

En el siguiente código, el primer método DoSomething sigue "patrón de plantilla". Llama al método virtual IsTokenChar. El segundo método DoSomthing no depende de la función virtual. En cambio, tiene un delegado de transferencia. En mi computadora, el primer DoSomthing siempre es más rápido que el segundo. El resultado es como 1645: 1780.

"Invocación virtual" es un enlace dinámico y debería costar más tiempo que la invocación de delegación directa, ¿verdad? pero el resultado muestra que no lo es.

¿Alguien puede explicar esto?

using System; 
using System.Diagnostics; 

class Foo 
{ 
    public virtual bool IsTokenChar(string word) 
    { 
     return String.IsNullOrEmpty(word); 
    } 

    // this is a template method 
    public int DoSomething(string word) 
    { 
     int trueCount = 0; 
     for (int i = 0; i < repeat; ++i) 
     { 
      if (IsTokenChar(word)) 
      { 
       ++trueCount; 
      } 
     } 
     return trueCount; 
    } 

    public int DoSomething(Predicate<string> predicator, string word) 
    { 
     int trueCount = 0; 
     for (int i = 0; i < repeat; ++i) 
     { 
      if (predicator(word)) 
      { 
       ++trueCount; 
      } 
     } 
     return trueCount; 
    } 

    private int repeat = 200000000; 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     Foo f = new Foo(); 

     { 
      Stopwatch sw = Stopwatch.StartNew(); 
      f.DoSomething(null); 
      sw.Stop(); 
      Console.WriteLine(sw.ElapsedMilliseconds); 
     } 

     { 
      Stopwatch sw = Stopwatch.StartNew(); 
      f.DoSomething(str => String.IsNullOrEmpty(str), null); 
      sw.Stop(); 
      Console.WriteLine(sw.ElapsedMilliseconds); 
     } 
    } 
} 
+2

Uno de Jon Skeet, me siento! ;) –

+0

@Mitch: en realidad no había visto tu comentario antes de responder, pero me siento halagado :) –

+0

Por cierto, la diferencia es que me siento más marcado con una compilación optimizada –

Respuesta

1

Es posible que ya no tiene ningún método que anulan el método virtual que el JIT es capaz de reconocer esto y utilizar una llamada directa en su lugar.

Para algo como esto generalmente es mejor probarlo como lo has hecho que intentar adivinar cuál será el rendimiento. Si desea saber más acerca de cómo funciona la invocación de delegados, sugiero el excelente libro "CLR Via C#" de Jeffrey Richter.

+0

No solo eso, pero si se almacenan/llaman delegados eran más eficientes que el despacho virtual, el marco casi seguro almacenaría y llamaría a los delegados en lugar de utilizar el despacho virtual. Dado que no es así, probablemente no lo sea (excepto en casos raros/patológicos). – technophile

+0

No creo que sea una diferencia que el método sea "virtual" en la clase principal o "sobrescribir" en la subclase. Si es "virtual", siempre es un enlace dinámico. –

-1

las anulaciones virtuales tienen algún tipo de tabla de redirección o algo que está codificado y totalmente optimizado en tiempo de compilación. Está escrito en piedra, muy rápido.

Los delegados son dinámicos, que siempre tendrán una sobrecarga y también parecen ser objetos, por lo que se acumulan.

No debe preocuparse por estas pequeñas diferencias de rendimiento (a menos que desarrolle software de rendimiento crítico para el ejército), para la mayoría de los propósitos, una buena estructura de código gana sobre la optimización.

+0

Corrección RÁPIDA: creo que quiso decir "reemplazos virtuales" en lugar de "sobrecargas virtuales" –

+0

Lo leí como "señores virtuales" la primera vez. – Andrew

+0

Whoops. ¡Entonces mi abuso intellisense se muestra! –

1

Dudo que represente toda su diferencia, pero una cosa fuera de lo común que puede explicar la diferencia es que el envío de métodos virtuales ya tiene el puntero this listo para funcionar. Al llamar a través de un delegado, el puntero this se debe recuperar del delegado.

Tenga en cuenta que según this blog article la diferencia fue aún mayor en .NET v1.x.

8

Una llamada virtual desreferencia dos punteros a un desplazamiento conocido en la memoria. En realidad no es un enlace dinámico; no hay código en tiempo de ejecución para reflejar los metadatos y descubrir el método correcto. El compilador genera un par de instrucciones para hacer la llamada, basado en este puntero. de hecho, la llamada virtual es una sola instrucción IL.

Una llamada de predicado está creando una clase anónima para encapsular el predicado. Esa clase tiene que crearse una instancia y se genera un código para verificar realmente si el puntero de la función de predicado es nulo o no.

Le sugiero que mire las construcciones de IL para ambos. Compile una versión simplificada de su fuente anterior con una sola llamada a cada uno de los dos DoSomthing.Luego use ILDASM para ver cuál es el código real para cada patrón.

(Y estoy seguro de que obtendrá downvoted para no usar la terminología correcta :-))

+0

En mi entender, "el método real para invocar se decide en el tiempo de ejecución" se llama "vinculación dinámica". –

+0

Un predicado es solo una instancia de delegado. Es una instancia, pero no estoy tan seguro de que haya una "clase anónima" creada. ¿Existe el concepto de "clase anónima" en C# /. NET? –

+0

Una llamada virtual no es "decidir qué método invocar en tiempo de ejecución". el método para invocar es bien conocido y ya está asociado con la clase. –

19

Piense en lo que se requiere en cada caso:

llamada virtual

  • Verificar la nulidad
  • Navegar desde el puntero del objeto al puntero de tipo
  • Loo k arriba la dirección del método en la tabla de instrucciones
  • (No estoy seguro - incluso Richter no cubre esto) Vaya al tipo base si el método no se reemplaza? Recurse hasta que encontremos la dirección de método correcta. (Yo no lo creo -. Editar ver en la parte inferior)
  • empuje puntero del objeto original en la pila ("este")
  • método de llamada

llamada Delegado

  • Comprobar la nulidad
  • Navegar desde el puntero del objeto a la matriz de invocaciones (todos los delegados son potencialmente multidifusión)
  • bucle sobre la matriz, y para cada invocación:
    • dirección de extracción de método
    • averiguar si o no para pasar el destino como primer argumento
    • argumentos de empuje contra la pila (puede haber sido hecho ya - no estoy seguro)
    • Opcionalmente (dependiendo de si la invocación es abierto o cerrado) empujar el objetivo invocación en la pila
    • método Call

Puede haber alguna optimización para que no exista ningún bucle en el caso de una sola llamada, pero aun así eso requerirá una verificación muy rápida.

Pero básicamente hay tanta indirección involucrada con un delegado. Dado el bit del que no estoy seguro en la llamada al método virtual, es posible que una llamada a un método virtual no verificado en una jerarquía de tipo masivo profundo sea más lenta ... Lo intentaré y edito con la respuesta.

EDIT: He intentado jugar con la jerarquía de profundidad de herencia (hasta 20 niveles), el punto de "anulación más derivada" y el tipo de variable declarada, y ninguno de ellos parece marcar la diferencia.

EDIT: Acabo de probar el programa original utilizando una interfaz (que se transfiere), que termina teniendo el mismo rendimiento que el delegado.

+0

En llamadas virtuales, no hay verificación de nulo. Además, el vtable de los métodos se determina en tiempo de compilación, por lo que no hay recursión en tiempo de ejecución sobre las clases base. El compilador genera el puntero al método correcto de la clase base adecuada y lo coloca en el espacio adecuado en el vtable. –

+6

callvirt * does * comprueba nulo: consulte la sección 4.2 de la partición 3 en la especificación CLI, o P166 de CLR a través de C#. (Si la referencia fuera nula, ¿a qué implementación se llamaría?) Sin embargo, gracias por la confirmación del bit "sin recursión". Eso es lo que el experimento básicamente sugirió. –

+3

+1 para la no recursión, los vtables se aplanan en tipo de compilación. – thinkbeforecoding

12

sólo quería añadir algunas correcciones a John al plato de respuesta:

Una llamada de método virtual no tiene que hacer una verificación nula (manejado de forma automática con trampas de hardware).

Tampoco necesita ir por la cadena de herencia para encontrar métodos no sustituidos (para eso sirve la tabla de métodos virtuales).

Una llamada al método virtual es esencialmente un nivel extra de indirección al invocar. Es más lento que una llamada normal debido a la búsqueda de tabla y la llamada de puntero de función posterior.

Una llamada de delegado también implica un nivel extra de indirección.

Las llamadas a un delegado no implican poner argumentos en una matriz a menos que esté realizando una invocación dinámica utilizando el método DynamicInvoke.

Una llamada de delegado implica el método de llamada que invoca un método de invocación generado por el compilador en el tipo de delegado en cuestión. Una llamada al predicador (valor) se convierte en predicador. Invocar (valor).

El método Invoke a su vez es implementado por el JIT para llamar al puntero (s) de función (almacenado internamente en el objeto delegado). En su ejemplo, el delegado que aprobó debería haberse implementado como método estático generado por el compilador ya que la implementación no tiene acceso a ninguna variable de instancia o local, por lo que no debe acceder al puntero "this" un problema.

La diferencia de rendimiento entre las llamadas a funciones delegadas y virtuales debería ser prácticamente la misma y las pruebas de rendimiento muestran que están muy cerca.

La diferencia podría deberse a la necesidad de cheques adicionales + sucursales debido a la multidifusión (como lo sugirió John). Otra razón podría ser que el compilador JIT no alinea el método Delegate.Invoke y la implementación de Delegate.Invoke no maneja los argumentos ni la implementación al realizar llamadas a métodos virtuales.

Cuestiones relacionadas