virtuales que puro puras polimorfismo objeto modificador herencia funciones c# performance delegates virtual

c# - que - polimorfismo puro c++



C#: ¿La invocación de funciones virtuales es incluso más rápida que una invocación de delegado? (7)

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 this puntero listo para funcionar. Al llamar a través de un delegado, this puntero debe obtenerse del delegado.

Tenga en cuenta que, de acuerdo con este artículo del blog, la diferencia fue aún mayor en .NET v1.x.

Simplemente me pasa sobre una pregunta 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.

Originalmente, pensé que el segundo modo de "delegado" sería más rápido que el modo "virtual", pero algunos fragmentos de código de programación 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.

La "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); } } }


Es posible que, dado que no tenga ningún método que anule el método virtual, el JIT pueda reconocerlo y, en su lugar, use una llamada directa.

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.


Piense en lo que se requiere en cada caso:

Llamada virtual

  • Verificar la nulidad
  • Navega desde el puntero del objeto hasta el puntero de tipo
  • Buscar la dirección del método en la tabla de instrucciones
  • (No estoy seguro - incluso Richter no cubre esto) ¿Ir al tipo base si el método no se reemplaza? Recurse hasta que encontremos la dirección de método correcta. (No lo creo - ver edición en la parte inferior.)
  • Empujar el puntero del objeto original a la pila ("esto")
  • Método de llamada

Llamada delegada

  • Verificar la nulidad
  • Navegue desde el puntero del objeto a la matriz de invocaciones (todos los delegados son potencialmente multidifusión)
  • Loop sobre array, y para cada invocación:
    • Obtener la dirección del método
    • Determine si pasar o no el objetivo como primer argumento
    • Empujar argumentos a la pila (puede que ya se haya hecho - no estoy seguro)
    • Opcionalmente (dependiendo de si la invocación está abierta o cerrada) presione el destino de invocación en la pila
    • Método de llamada

Puede haber algo de 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.

EDITAR: He intentado jugar con la jerarquía de profundidad de la herencia (hasta 20 niveles), el punto de "la mayoría de las derivaciones derivadas" y el tipo de variable declarada, y ninguno de ellos parece marcar la diferencia.

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


Una llamada virtual está desmarcando dos punteros en 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.

Sugeriría que mires los constructos 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 recibiré una votación negativa por no usar la terminología correcta :-))


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, de modo 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.


Solo quería agregar algunas correcciones a la respuesta de John skeet:

Una llamada a un método virtual no necesita hacer una comprobación nula (se maneja automáticamente con trampas de hardware).

Tampoco es necesario que vaya por la cadena de herencia para encontrar métodos no sustituidos (para eso sirve la tabla de métodos virtuales).

Una llamada a un método virtual es esencialmente un nivel adicional 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 que el método de llamada invoque un método Invoke generado por el compilador en el tipo de delegado en cuestión. Una llamada al predicador (valor) se convierte en predicador. Invocar (valor).

El JIT implementa el método Invoke para llamar al (los) puntero (s) de función (almacenado internamente en el objeto delegado).

En su ejemplo, el delegado que pasó debería haberse implementado como un método estático generado por el compilador, ya que la implementación no tiene acceso a variables de instancia ni locales, por lo que la necesidad de acceder al puntero "this" desde el montón no debería ser un problema.

La diferencia de rendimiento entre delegado y llamadas a funciones virtuales debe ser prácticamente la misma y sus pruebas de rendimiento muestran que están muy cerca.

La diferencia podría deberse a la necesidad de comprobaciones 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.