2009-03-24 10 views

Respuesta

8

Una llamada virtual C# debe comprobar si "this" es nulo y si una llamada virtual C++ no lo hace. Así que no puedo ver en general por qué una llamada virtual C# sería más rápida. En casos especiales, el compilador C# (o el compilador JIT) puede ser capaz de alinear la llamada virtual mejor que un compilador C++, ya que un compilador C# tiene acceso a un mejor tipo de información. La instrucción de método de llamada puede algunas veces ser más lenta en C++, ya que C# JIT puede usar instrucciones más rápidas que solo resuelven un pequeño desplazamiento ya que sabe más sobre el diseño de memoria de tiempo de ejecución y el modelo de procesador que un compilador de C++.

Sin embargo, aquí estamos hablando de un puñado de instrucciones de procesador. En un procesador superescalar de módem, es muy posible que la instrucción de "comprobación nula" se ejecute al mismo tiempo que el "método de llamada" y, por lo tanto, no tome tiempo.

También es muy probable que todas las instrucciones del procesador ya estén en el nivel 1 de la memoria caché si la llamada se realiza en un bucle. Pero es menos probable que los datos sean cachés, el costo de leer un valor de datos de la memoria principal en estos días es lo mismo que ejecutar cientos de instrucciones desde el caché de nivel 1. Por lo tanto, es desafortunado que en aplicaciones reales el costo de una llamada virtual sea incluso medible en más de unos pocos lugares.

El hecho de que el código C# use algunas instrucciones más reducirá, por supuesto, la cantidad de código que puede caber en la memoria caché, el efecto de esto es imposible de predecir.

(Si la clase de C++ utiliza múltiples inherencia entonces el costo es más, debido a tener que arreglar el puntero “this”. Del mismo modo interfaces en C# añadir otro nivel de redirección.)

2

El costo de una llamada virtual en C++ es el de una llamada de función a través de un puntero (vtbl). Dudo de que C# puede hacer que uno más rápido y aún así ser capaz de determinar el tipo de objeto en tiempo de ejecución ...

Editar: Como Pete Kirkham señaló, una buena JIT podría ser capaz de inline de C# de llamadas, evitando puesto de tubería; algo que la mayoría de los compiladores de C++ no pueden hacer (todavía). Por otro lado, Ian Ringrose mencionó el impacto en el uso del caché. Además de eso, el propio JIT se está ejecutando, y (estrictamente personalmente) no me molestaría realmente a menos que el perfil en la máquina de destino bajo cargas de trabajo realistas haya probado que es más rápido que el otro. Es una micro-optimización en el mejor de los casos.

3

Supongo que esta suposición se basa en el compilador JIT, lo que significa que C# probablemente convierta una llamada virtual en una simple llamada a método antes de que realmente se use.

¡Pero es esencialmente teórico y no apostaría!

+0

Incluso si este fuera el caso; que "convertir en una simple llamada a método" tampoco es gratis, ¿verdad? – DevSolar

+0

No, por supuesto que no. Pero no tendría que pagar nada al hacer la llamada real (como pagar por adelantado). –

+0

Point es, la próxima vez que realice esa llamada, tendrá que verificar el objeto nuevamente. En general, esto podría cambiar, por lo que obj.foo() se refiere a un foo diferente cada vez. Tenga en cuenta que los compiladores de C++ a menudo también pueden convertir la llamada virtual a una llamada normal, si el tipo de objeto se conoce en tiempo de compilación. – MSalters

1

No estoy seguro acerca del marco completo pero en el Marco Compacto será más lento porque CF no tiene tablas de llamadas virtuales aunque almacena el resultado en caché. Esto significa que una llamada virtual en CF será más lenta la primera vez que se invoca ya que tiene que hacer una búsqueda manual. Puede ser lenta cada vez que se invoca si la aplicación tiene poca memoria, ya que la búsqueda en caché puede estar activada.

5

Para los lenguajes compilados JIT (no sé si CLR lo hace o no, Sun's JVM sí), es una optimización común convertir una llamada virtual que tiene solo dos o tres implementaciones en una secuencia de pruebas del tipo y llamadas directas o en línea. La ventaja de esto es que las CPU modernas pueden usar la predicción de bifurcación y la captación previa de llamadas directas, pero una llamada indirecta (representada por un puntero a función en lenguajes de alto nivel) a menudo resulta en el estancamiento de la tubería.

En el caso límite, cuando solo hay una implementación de la llamada virtual y el cuerpo de la llamada es lo suficientemente pequeño, la llamada virtual se reduce a puramente inline code. Esta técnica se usó en el tiempo de ejecución Self language, del que evolucionó la JVM.

La mayoría de los compiladores de C++ no realizan el análisis de programa completo requerido para realizar esta optimización, pero proyectos como LLVM están buscando optimizaciones de todo el programa como esta.

+0

¿Estás seguro de que siempre provoca un bloqueo de la tubería? No hay ninguna razón por la que una CPU no pueda captar previamente una llamada indirecta (y me imagino que es algo que Intel apuntaría específicamente ...). Si se puede precargar, la sobrecarga de una función virtual será cero. –

+0

Hace unos años, lo revisé por última vez, por lo que podría estar equivocado. La única referencia que puedo encontrar en Intel para predecir ramas indirectas está en su compilación dirigida de perfil; la mayoría de sus documentos solo dicen que son "muy difíciles de predecir", y otra investigación dice que el 99% de los puestos son de llamadas indirectas. –

+0

Creo que la segunda vez que se tome la misma llamada indirecta desde el mismo sitio de llamadas, no habrá un bloqueo. Por ejemplo, si un ciclo está haciendo una llamada virtual en muchos objetos del mismo tipo, estará bien. –

0

En C# podría ser posible convertir una función virtual a no virtual analizando el código. En la práctica, no sucederá con la suficiente frecuencia como para hacer mucha diferencia.

0

C# aplana las llamadas de ancestro vtable y en línea para que no encadena la jerarquía de herencia para resolver nada.

0

Puede que no sea exactamente la respuesta a su pregunta, pero aunque .NET JIT optimiza las llamadas virtuales como todo el mundo dijo anteriormente, profile-guided optimization en Visual Studio 2005 y 2008 hace especulación de llamadas virtuales insertando una llamada directa al objetivo más probable función, alineando la llamada, por lo que el peso puede ser el mismo.

4

La pregunta original dice:

Me parece recordar haber leído en alguna parte que el costo de una llamada virtual en C# no es tan alta, relativamente hablando , como en C++.

Tenga en cuenta el énfasis. En otras palabras, la pregunta podría ser reformulada como:

Me parece recordar haber leído en alguna parte que en C#, virtuales y no virtuales llamadas son igualmente lento, mientras que en C++ una llamada virtual es más lento que una llamada no virtual ...

Así que el que pregunta no afirma que C# sea más rápido que C++ bajo ninguna circunstancia.

Posiblemente una diversión inútil, pero esto despertó mi curiosidad sobre C++ con/clr: pure, sin extensiones C++/CLI. El compilador produce IL que el JIT convierte en código nativo, aunque es puro C++. Así que aquí tenemos una forma de ver lo que hace una implementación estándar de C++ si se ejecuta en la misma plataforma que C#.

Con un método no virtual:

struct Plain 
{ 
    void Bar() { System::Console::WriteLine("hi"); } 
}; 

este código:

Plain *p = new Plain(); 
p->Bar(); 

... hace que el call código de operación que se emitirá con el nombre de método específico, pasando un bar this argumento implícito .

call void <Module>::Plain.Bar(valuetype Plain*) 

Comparar con una jerarquía de herencia:

struct Base 
{ 
    virtual void Bar() = 0; 
}; 

struct Derived : Base 
{ 
    void Bar() { System::Console::WriteLine("hi"); } 
}; 

Ahora bien, si hacemos:

Base *b = new Derived(); 
b->Bar(); 

que emite el código de operación calli lugar, que salta a una dirección computarizada - así que hay un montón de IL antes de la llamada. Girándolo en el sentido de nuevo a C# podemos ver lo que está pasando:

**(*((int*) b))(b); 

En otras palabras, echar la dirección de b a un puntero a int (que pasa a ser del mismo tamaño que un puntero) y tomar el valor en esa ubicación, que es la dirección del vtable, y luego toma el primer elemento en el vtable, que es la dirección a la que ir, lo desreferencia y lo llama, pasando el argumento implícito this.

Podemos ajustar el ejemplo virtual para utilizar extensiones C++/CLI:

ref struct Base 
{ 
    virtual void Bar() = 0; 
}; 

ref struct Derived : Base 
{ 
    virtual void Bar() override { System::Console::WriteLine("hi"); } 
}; 

Base ^b = gcnew Derived(); 
b->Bar(); 

Esto genera el código de operación callvirt, exactamente como lo haría en C#:

callvirt instance void Base::Bar() 

Así que cuando se compila para apuntar a los CLR, el compilador C++ actual de Microsoft no tiene las mismas posibilidades de optimización que C# cuando usa las características estándar de cada idioma; para una jerarquía de clases estándar de C++, el compilador de C++ genera código que contiene una lógica codificada para atravesar el vtable, mientras que para una clase de referencia le deja al JIT descubrir la implementación óptima.

+0

Esto se trata de C++ en la parte superior de la CLR, que en realidad no es fair play en mi humilde opinión. – DevSolar

+0

Además, ¿qué significa "justo" en este contexto? –

+0

Johann preguntó sobre las llamadas virtuales en C# siendo "más barato" que en C++. Tomo "C++" para significar "C++ compilado a nativo". Entonces MSC++ no puede compilar una llamada virtual en C++ en IL "callvirt". El "¿por qué no?" tiene que estar dirigido al compilador de MS, no al lenguaje, ¿no es así? – DevSolar