¿Cuál es la diferencia entre las instrucciones CIL "Llamar" y "Callvirt"?Call and Callvirt
Respuesta
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).
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.
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
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
Eso tiene sentido. Estudié CIL e hice una suposición sobre el comportamiento del compilador. Gracias por aclarar esto. Actualizaré mi respuesta. –
En realidad, no creo que estés 100% correcto aquí. He actualizado mi publicación (edición 2). –
Hola Cameron. ¿Puedes aclarar si el análisis de este código se realizó en una compilación de lanzamiento? –
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).
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
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
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/
'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)). –
- 1. Fire and Forget (Asynch) ASP.NET Method Call
- 2. .NET CIL ¿Llamada o CallVirt?
- 3. erlang call stack
- 4. call gettid witin glibc
- 5. No entiendo Jinja2 Call Blocks
- 6. (OrElse and Or) and (AndAlso and And) - ¿Cuándo usar?
- 7. Vi - ": call append()"
- 8. Android Handle phone call
- 9. onCreateDrawableState never call
- 10. Cómo inspeccionar Call Stack
- 11. Imprimir PHP Call Stack
- 12. FileSystemWatcher Dispose call cuelga
- 13. Detalles de call/cc
- 14. IllegalMonitorStateException en wait() call
- 15. ¿Qué es call/cc?
- 16. Fake Incoming Call Android
- 17. javascript setTimeout call error
- 18. ¿Por qué el compilador C# emite una instrucción callvirt para una llamada al método GetType()?
- 19. Cómo obtener el método realmente llamado por la instrucción callvirt IL dentro de FxCop
- 20. return from jquery ajax call
- 21. patrones "call-cc" en Scala?
- 22. android ksoap call xmlpullparser excepción
- 23. JQuery dominio cruzado auth call
- 24. Configurando AssemblyFileVersion con MSBuild-call?
- 25. cross-domain AJAX post call
- 26. Método anónimo en Invoke call
- 27. ¿Cómo funciona 'call' en javascript?
- 28. Script Call Perl de Python
- 29. flags para st_mode of stat system call
- 30. struct and class and inheritance (C++)
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
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). –
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. –