2008-10-11 11 views

Respuesta

42

call es para llamar a métodos no virtuales, estáticos o de superclase, es decir, el objetivo de la llamada no está sujeto a anulación. callvirt es para llamar a métodos virtuales (de modo que si this es una subclase que anula el método, en su lugar se llama a la versión de la subclase).

+25

Si recuerdo correctamente 'call' no comprueba el puntero contra null antes de realizar la llamada, algo que' callvirt' obviamente necesita. Por eso 'callvirt' a veces es emitido por el compilador aunque llame a métodos no virtuales. – dalle

+1

Ah, gracias por señalar eso (no soy un Persona NET). Las analogías que uso son call => invokespecial, y callvirt => invokevirtual, en bytecode de JVM. En el caso de la JVM, ambas instrucciones marcan "esto" para la nulidad (acabo de escribir un programa de prueba para verificar). –

+2

Es posible que desee mencionar la diferencia de rendimiento en su respuesta, que es la razón para tener una instrucción de 'llamada' en absoluto. –

45

Cuando el tiempo de ejecución ejecuta una instrucción call, realiza una llamada a un fragmento exacto de código (método). No hay dudas sobre dónde existe. Una vez que el IL ha sido JITted, el código de la máquina resultante en el sitio de la llamada es una instrucción incondicional jmp.

Por el contrario, la instrucción callvirt se utiliza para llamar a métodos virtuales de forma polimórfica. La ubicación exacta del código del método se debe determinar en el tiempo de ejecución para cada invocación. El código JITted resultante implica algo de indirección a través de estructuras vtable. Por lo tanto, la llamada es más lenta de ejecutar, pero es más flexible ya que permite llamadas polimórficas.

Tenga en cuenta que el compilador puede emitir call instrucciones para métodos virtuales. Por ejemplo:

sealed class SealedObject : object 
{ 
    public override bool Equals(object o) 
    { 
     // ... 
    } 
} 

Considere Código de llamada:

SealedObject a = // ... 
object b = // ... 

bool equal = a.Equals(b); 

Mientras System.Object.Equals(object) es un método virtual, en este uso no hay manera para que una sobrecarga del método Equals de existir. SealedObject es una clase sellada y no puede tener subclases.

Por esta razón, las clases sealed de .NET pueden tener un mejor rendimiento en el envío de métodos que sus contrapartes no selladas.

EDIT: Resulta que estaba equivocado. El compilador de C# no puede hacer un salto incondicional a la ubicación del método porque la referencia del objeto (el valor de this dentro del método) puede ser nulo. En su lugar, emite callvirt que hace la comprobación nula y lo arroja si es necesario.

En realidad, esto explica algo de código extraño que encontré en el marco .NET usando Reflector:

if (this==null) // ... 

Es posible que un compilador para emitir el código comprobable de que tiene un valor nulo para el puntero this (local0), sólo se csc no hace esto.

Supongo que call solo se usa para métodos y estructuras estáticos de clase.

Dada esta información, ahora me parece que sealed solo es útil para la seguridad de la API. Encontré another question que parece sugerir que no hay beneficios de rendimiento para sellar sus clases.

EDIT 2: Hay más en esto de lo que parece. Por ejemplo el siguiente código emite una instrucción de call:

new SealedObject().Equals("Rubber ducky"); 

Obviamente, en este caso, no hay ninguna posibilidad de que la instancia del objeto podría ser nulo.

Curiosamente, en una versión de depuración, el siguiente código emite callvirt:

var o = new SealedObject(); 
o.Equals("Rubber ducky"); 

Esto se debe a que podría establecer un punto de interrupción en la segunda línea y modificar el valor de o. En las compilaciones de lanzamiento, imagino que la llamada sería call en lugar de callvirt.

Desafortunadamente, mi PC está actualmente fuera de acción, pero voy a experimentar con esto una vez que vuelva a funcionar.

+2

Los atributos sellados son definitivamente más rápidos al buscarlos mediante la reflexión, pero aparte de eso, no sabe de ningún otro beneficio que no haya mencionado. – TraumaPony

11

Por esta razón, las clases selladas de .NET pueden tener un mejor rendimiento de envío de métodos que sus contrapartes no selladas.

Desafortunadamente este no es el caso. Callvirt hace otra cosa que lo hace útil. Cuando un objeto tiene un método invocado, callvirt comprobará si el objeto existe, y si no arroja una NullReferenceException. La llamada simplemente saltará a la ubicación de la memoria incluso si la referencia del objeto no está allí, y tratará de ejecutar los bytes en esa ubicación.

Lo que esto significa es que callvirt siempre es utilizado por el compilador C# (no seguro de VB) para las clases, y la llamada siempre se usa para las estructuras (porque nunca pueden ser nulas o subclasificadas).

Editar En respuesta al comentario de Drew Noakes: Sí parece que se puede obtener el compilador emita una llamada por cualquier clase, pero sólo en el siguiente caso muy específico:

public class SampleClass 
{ 
    public override bool Equals(object obj) 
    { 
     if (obj.ToString().Equals("Rubber Ducky", StringComparison.InvariantCultureIgnoreCase)) 
      return true; 

     return base.Equals(obj); 
    } 

    public void SomeOtherMethod() 
    { 
    } 

    static void Main(string[] args) 
    { 
     // This will emit a callvirt to System.Object.Equals 
     bool test1 = new SampleClass().Equals("Rubber Ducky"); 

     // This will emit a call to SampleClass.SomeOtherMethod 
     new SampleClass().SomeOtherMethod(); 

     // This will emit a callvirt to System.Object.Equals 
     SampleClass temp = new SampleClass(); 
     bool test2 = temp.Equals("Rubber Ducky"); 

     // This will emit a callvirt to SampleClass.SomeOtherMethod 
     temp.SomeOtherMethod(); 
    } 
} 

NOTA El la clase no tiene que estar sellada para que esto funcione.

lo que parece que el compilador emitirá una llamada si todas estas cosas son ciertas:

  • La llamada al método es inmediatamente después de la creación del objeto
  • El método no se implementa en una clase base
+0

Eso tiene sentido. Estudié CIL e hice una suposición sobre el comportamiento del compilador. Gracias por aclarar esto. Actualizaré mi respuesta. –

+1

En realidad, no creo que estés 100% correcto aquí. He actualizado mi publicación (edición 2). –

+0

Hola Cameron. ¿Puedes aclarar si el análisis de este código se realizó en una compilación de lanzamiento? –

5

de acuerdo con MSDN:

Call:

La instrucción de llamada llama al método indicado por el descriptor de método pasado con la instrucción. El descriptor de método es un token de metadatos que indica el método para llamar ... El token de metadatos contiene suficiente información para determinar si la llamada es a un método estático, un método de instancia, un método virtual o una función global. En todos estos casos, la dirección de destino se determina completamente desde el descriptor de método (contraste esto con la instrucción Callvirt para llamar a métodos virtuales, donde la dirección de destino también depende del tipo de tiempo de ejecución de la referencia de instancia antes de Callvirt).

CallVirt:

La instrucción callvirt llama a un método de enlace en tiempo de un objeto. Es decir, , el método se elige en función del tipo de tiempo de ejecución de obj en lugar de la clase de tiempo de compilación visible en el puntero de método.Callvirt se puede usar para llamar a métodos virtuales e instancia.

Así que, básicamente, diferentes rutas se toman para invocar método de instancia de un objeto, anulado o no:

Llamar: Variable ->de variables tipo de objeto -> método

CallVirt: Variable -> instancia de objeto ->del objeto tipo de objeto -> método

2

una cosa tal vez vale la pena añadir a las respuestas anteriores es, no parece ser sólo una cara de la forma "llamada IL" realmente ejecuta, y dos caras de cómo se ejecuta "IL callvirt".

Tome esta configuración de muestra.

public class Test { 
     public int Val; 
     public Test(int val) 
      { Val = val; } 
     public string FInst() // note: this==null throws before this point 
      { return this == null ? "NO VALUE" : "ACTUAL VALUE " + Val; } 
     public virtual string FVirt() 
      { return "ALWAYS AN ACTUAL VALUE " + Val; } 
    } 
    public static class TestExt { 
     public static string FExt (this Test pObj) // note: pObj==null passes 
      { return pObj == null ? "NO VALUE" : "VALUE " + pObj.Val; } 
    } 

En primer lugar, el cuerpo CIL de finst() y FEXT() es 100% idéntica, opcode-a-opcode (excepto que uno se declara "ejemplo" y el otro "estático") - sin embargo, se llamará a FInst() con "callvirt" y FExt() con "call".

En segundo lugar, finst() y fVirt() será tanto ser llamada con "callvirt" - a pesar de que uno es virtual, pero el otro no es - pero no es la "misma callvirt" que conseguirá realmente ejecutar.

Esto es lo que ocurre más o menos después de JITting:

pObj.FExt(); // IL:call 
    mov   rcx, <pObj> 
    call  (direct-ptr-to) <TestExt.FExt> 

    pObj.FInst(); // IL:callvirt[instance] 
    mov   rax, <pObj> 
    cmp   byte ptr [rax],0 
    mov   rcx, <pObj> 
    call  (direct-ptr-to) <Test.FInst> 

    pObj.FVirt(); // IL:callvirt[virtual] 
    mov   rax, <pObj> 
    mov   rax, qword ptr [rax] 
    mov   rax, qword ptr [rax + NNN] 
    mov   rcx, <pObj> 
    call  qword ptr [rax + MMM] 

La única diferencia entre "llamada" y "callvirt [instancia]" es que "callvirt [instancia]" intenta intencionadamente para acceder a un byte de * pObj antes llama al puntero directo de la función de instancia (para posiblemente lanzar una excepción "allí mismo y luego").

Por lo tanto, si usted está molesto por el número de veces que hay que escribir la "parte comprobación" de

var d = GetDForABC (a, b, c); 
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E; 

No se puede empujar "si (esto == null) SOME_DEFAULT_E retorno;" hasta ClassD.GetE() en sí (como la semántica "IL callvirt [instance]" le prohibe hacer esto) pero puede insertarlo en .GetE() si mueve .GetE() a una extensión funcionar en alguna parte (como lo permite la semántica "llamada IL", pero lamentablemente, perder acceso a miembros privados, etc.)

Dicho esto, la ejecución de "callvirt [instancia]" tiene más en común con "llamada" que con "callvirt [virtual]", ya que este último puede tener que ejecutar un triple direccionamiento indirecto para encontrar la dirección de su función. (dirección a typedef base, a continuación, a base VTAB-o-alguna-interfaz, a continuación, a la ranura real)

Espero que esto ayude, Boris

1

Simplemente añadiendo a las respuestas anteriores, creo que el cambio tiene hace mucho tiempo que se genera la instrucción Callvirt IL para todos los métodos de instancia y se genera la instrucción Call IL para los métodos estáticos.

Referencia:

Pluralsight curso "C Internos # Idioma - Parte 1 por Bart De Smet (vídeo - Call instrucciones y llame pilas en CLR IL en una cáscara de nuez)

y también https://blogs.msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/

+0

'call' también se usa para llamadas base. Además, creo que el compilador de roslyn generará instrucciones de 'llamada' para métodos sellados/no virtuales si puede determinar trivialmente que el objeto nunca será nulo (por ejemplo, [llamadas generadas desde el operador condicional nulo] (http: // stackoverflow.com/questions/34535000/call-instead-of-callvirt-in-case-of-the-new-c-sharp-6-null-check)). –