Chamada e callvirt
-
10-07-2019 - |
Pergunta
Qual é a diferença entre as instruções CIL "Call" e "callvirt"?
Solução
call
é para chamar não-virtuais, métodos estático, ou superclasse, ou seja, o alvo da chamada não está sujeita a substituir. callvirt
é para chamar métodos virtuais (para que se this
é uma subclasse que substitui o método, a versão subclasse é chamado em seu lugar).
Outras dicas
Quando o tempo de execução executa uma instrução call
ele está fazendo uma chamada para uma peça exata do código (método). Não há nenhuma pergunta sobre onde ele existe. Uma vez que a IL foi JITted, o código de máquina, resultando no site da chamada é uma instrução jmp
incondicional.
Por outro lado, a instrução callvirt
é usado para chamar métodos virtuais de uma forma polimórfica. A localização exata do código do método deve ser determinada em tempo de execução para cada invocação. O código JITted resultando envolve algum engano através de estruturas vtable. Daí a chamada é mais lento para executar, mas é mais flexível na medida em que permite chamadas polimórficas.
Note que o compilador pode emitir instruções call
para métodos virtuais. Por exemplo:
sealed class SealedObject : object
{
public override bool Equals(object o)
{
// ...
}
}
Considere chamando código:
SealedObject a = // ...
object b = // ...
bool equal = a.Equals(b);
Enquanto System.Object.Equals(object)
é um método virtual, neste uso não há nenhuma maneira para uma sobrecarga do método Equals
de existir. SealedObject
é uma classe selada e não pode ter subclasses.
Por esta razão, da NET aulas sealed
pode ter um melhor método de envio de desempenho do que suas contrapartes não-selado.
EDIT: Acontece que eu estava errado. O compilador C # não pode fazer um salto incondicional para a localização do método porque de referência do objeto (o valor de this
dentro do método) pode ser nulo. Em vez disso, emite callvirt
que faz a verificação de nulo e joga, se necessário.
Este facto explica algum código bizarro eu encontrei no framework .NET usando refletor:
if (this==null) // ...
É possível para um compilador para emitir código verificável que tem um valor nulo para o ponteiro this
(local0), apenas csc não faz isso.
Então eu acho que call
é usada apenas para os métodos de classe estáticos e estruturas.
Dada esta informação agora parece-me que sealed
só é útil para a segurança API. Eu encontrei outra pergunta que parece sugerir não há benefícios de desempenho para selando suas classes.
EDIT 2: Há mais a este do que parece. Por exemplo, o seguinte código emite uma instrução call
:
new SealedObject().Equals("Rubber ducky");
Obviamente, nesse caso, não há nenhuma chance de que a instância do objeto poderia ser nulo.
Curiosamente, em uma compilação de depuração, os seguintes emite código callvirt
:
var o = new SealedObject();
o.Equals("Rubber ducky");
Isso é porque você pode definir um ponto de interrupção na segunda linha e modificar o valor de o
. Nas compilações imagino a chamada seria um call
em vez de callvirt
.
Infelizmente o meu PC está atualmente fora de ação, mas vou experimentar com este, uma vez que é para cima novamente.
Por esta razão, da NET aulas fechados pode ter um melhor método de envio de desempenho do que suas contrapartes não-selado.
Infelizmente, este não é o caso. Callvirt faz uma outra coisa que o torna útil. Quando um objeto tem um método chamado nele callvirt irá verificar se o objeto existe, e se não lança um NullReferenceException. Chamada simplesmente saltar para o local de memória mesmo se a referência de objeto não está lá, e tentar executar os bytes nesse local.
O que isto significa é que callvirt é sempre usado pelo compilador C # (não tenho certeza sobre VB) para as aulas, e chamada é sempre usado para estruturas (porque nunca pode ser nulo ou subclasse).
Editar Em resposta ao comentário de Drew Noakes: Sim, parece que você pode obter o compilador para emitir uma chamada para qualquer classe, mas apenas na seguinte caso muito 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 A classe não tem que ser selado para que isso funcione.
Portanto, parece que o compilador irá emitir uma chamada, se todas estas coisas são verdadeiras:
- A chamada do método é imediatamente após a criação do objeto
- O método não é implementado em uma classe base
De acordo com MSDN:
Chamada :
A instrução de chamada chama o método indicado pelo descritor método passado com a instrução. O descritor de método é um metadados de token que indica o método para chamar ... Os metadados de token transporta informações suficientes para determinar se a chamada for para um método estático, um método de instância, um método virtual, ou uma função global. Em todos esses casos, o endereço de destino é determinado inteiramente a partir do descritor método (contrastar isso com a instrução callvirt para chamar métodos virtuais, onde o endereço de destino também depende do tipo de tempo de execução da referência de instância empurrado antes o callvirt).
callvirt :
A instrução callvirt chama um método de ligação tardia em um objeto. Ou seja, o método é escolhido com base no tipo de tempo de execução de obj em vez da classe em tempo de compilação visível no ponteiro método . Callvirt pode ser usado para chamar os dois métodos virtuais e de instância.
Então, basicamente, diferentes rotas são levados para invocar método de instância de um objeto, anulado ou não:
Chamada: variável -> variável tipo de objeto -> método
callvirt: variável -> instância do objeto -> objeto tipo de objeto -> método
Uma coisa talvez pena acrescentar às respostas anteriores é, parece haver apenas uma face à forma como "chamada IL" realmente executa, e duas faces à forma como executa "IL callvirt".
Tome esta configuração de exemplo.
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; }
}
Primeiro, o corpo de CIL FInst () e fext () é 100% idêntica, opcode-se opcode (Exceto que um é declarado "instância" eo outro "estático") -. No entanto, FInst () será chamado com "callvirt" e FEXT () com "chamada"
Em segundo lugar, FInst () e FVirt (), ambos serão chamados com "callvirt" - mesmo que um é virtual, mas o outro não é - mas não é o "mesmo callvirt" que realmente vai começar a executar.
Aqui está o que acontece depois de aproximadamente 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]
A única diferença entre "chamada" e "callvirt [exemplo]" é que "callvirt [exemplo]" tenta intencionalmente para acesso de um byte de * pObj antes de ele chama o ponteiro direto da função instância (a fim de, eventualmente, jogar uma exceção "ali mesmo").
Assim, se você está irritado com o número de vezes que você tem que escrever a "parte verificação" de
var d = GetDForABC (a, b, c);
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E;
Você não pode empurrar "se (este == null) return SOME_DEFAULT_E;" para baixo em ClassD.GetE () em si (Como o "callvirt IL [exemplo]" semântica proíbe você de fazer isso) mas você é livre para empurrá-lo para .GetE () se você mover .GetE () para um lugar função de extensão (como a "chamada IL" semântica permite que ele -. Mas, infelizmente, perder o acesso aos membros privados etc)
Dito isto, a execução de "callvirt [exemplo]" tem mais em comum com "chamada" do que com "callvirt [virtual]", uma vez que este pode ter que executar um engano triplo a fim de encontrar o endereço de sua função. (Indirecta para typedef de base, em seguida, a base de-vtab-ou-algum interface, em seguida, a ranhura real)
Espero que isso ajude, Boris
Apenas somando-se as respostas acima, acho que a alteração foi feita há muito tempo tais que a instrução callvirt IL será gerada para todos os métodos de instância e instrução de chamada IL vai ser gerados por métodos estáticos.
Referência:
Pluralsight curso "Linguagem C # Internals - Parte 1 por Bart De Smet (vídeo - Chamada instruções e pilhas de chamadas em CLR IL em um Nutshell)
e também https: // blogs .msdn.microsoft.com / ericgu / 2008/07/02 / why-faz-c-sempre-use-callvirt /