Pergunta

Qual é a diferença entre as instruções CIL "Call" e "callvirt"?

Foi útil?

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 /

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top