67 votes

Call et Callvirt

Quelle est la différence entre les instructions du CIL "Call" et "Callvirt" ?

63voto

Drew Noakes Points 69288

Lorsque le runtime exécute un call l'instruction consiste à faire un appel à un morceau de code exact (méthode). Il n'y a aucun doute sur l'endroit où il existe. Une fois que l'IL a été JITté, le code machine résultant au niveau du site d'appel est un code inconditionnel jmp l'instruction.

En revanche, le callvirt est utilisée pour appeler des méthodes virtuelles de manière polymorphe. L'emplacement exact du code de la méthode doit être déterminé au moment de l'exécution pour chaque invocation. Le code JIT résultant implique une certaine indirection à travers les structures vtable. L'appel est donc plus lent à exécuter, mais il est plus flexible car il permet des appels polymorphes.

Notez que le compilateur peut émettre call des instructions pour les méthodes virtuelles. Par exemple :

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

Pensez à appeler le code :

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

bool equal = a.Equals(b);

Alors que System.Object.Equals(object) est une méthode virtuelle, dans cette utilisation, il n'y a pas de possibilité de surcharge de la méthode Equals pour exister. SealedObject est une classe scellée et ne peut pas avoir de sous-classes.

C'est pour cette raison que l'approche de .NET sealed peuvent avoir de meilleures performances de répartition des méthodes que leurs homologues non scellés.

EDITAR: Il s'avère que j'avais tort. Le compilateur C# ne peut pas faire un saut inconditionnel vers l'emplacement de la méthode parce que la référence de l'objet (la valeur de this dans la méthode) peut être nulle. Au lieu de cela, il émet callvirt qui effectue la vérification de la nullité et lance l'appel si nécessaire.

Cela explique en fait un code bizarre que j'ai trouvé dans le cadre .NET en utilisant Reflector :

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

Il est possible pour un compilateur d'émettre un code vérifiable qui a une valeur nulle pour l'attribut this pointeur (local0), seul csc ne le fait pas.

Donc je suppose call n'est utilisé que pour les méthodes statiques de classe et les structs.

Compte tenu de ces informations, il me semble maintenant que sealed n'est utile que pour la sécurité de l'API. J'ai trouvé une autre question qui semble suggérer qu'il n'y a aucun avantage de performance à sceller vos classes.

EDIT 2 : Il y a plus qu'il n'y paraît. Par exemple, le code suivant émet un call l'instruction :

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

Évidemment, dans un tel cas, il n'y a aucune chance que l'instance de l'objet soit nulle.

Il est intéressant de noter que dans un build DEBUG, le code suivant émet callvirt :

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

En effet, vous pourriez placer un point d'arrêt sur la deuxième ligne et modifier la valeur de o . Dans les builds de la version, j'imagine que l'appel serait un call plutôt que callvirt .

Malheureusement, mon PC est actuellement hors service, mais je ferai l'expérience dès qu'il sera remis en marche.

4 votes

Les attributs scellés sont définitivement plus rapides lorsqu'ils sont recherchés par réflexion, mais à part cela, je ne connais pas d'autres avantages que vous n'avez pas mentionnés.

54voto

Chris Jester-Young Points 102876

call est utilisé pour appeler des méthodes non virtuelles, statiques ou de superclasse, autrement dit, la cible de l'appel n'est pas sujette à une substitution. callvirt est utilisé pour appeler des méthodes virtuelles (de sorte que si this est une sous-classe qui remplace la méthode, la version de la sous-classe est appelée à la place).

43 votes

Si je me souviens bien call ne vérifie pas l'absence de nullité du pointeur avant d'effectuer l'appel, ce que callvirt en a manifestement besoin. C'est pourquoi callvirt est parfois émise par le compilateur même si elle appelle des méthodes non virtuelles.

2 votes

Ah. Merci de me l'avoir fait remarquer (je ne suis pas un spécialiste de .NET). Les analogies que j'utilise sont call => invokespecial, et callvirt => invokevirtual, dans le bytecode JVM. Dans le cas de la JVM, les deux instructions vérifient la nullité de "this" (je viens d'écrire un programme de test pour le vérifier).

3 votes

Vous pourriez mentionner la différence de performance dans votre réponse, car c'est la raison pour laquelle il existe une instruction "call".

13voto

Cameron MacFarland Points 27240

Pour cette raison, les classes scellées de .NET peuvent avoir de meilleures performances de distribution de méthodes que leurs homologues non scellées.

Malheureusement, ce n'est pas le cas. Callvirt fait une autre chose qui le rend utile. Lorsqu'une méthode est appelée sur un objet, Callvirt vérifie si l'objet existe, et si ce n'est pas le cas, il lève une NullReferenceException. Callvirt sautera simplement à l'emplacement de la mémoire même si la référence de l'objet n'est pas là, et essaiera d'exécuter les octets dans cet emplacement.

Cela signifie que callvirt est toujours utilisé par le compilateur C# (pas sûr pour VB) pour les classes, et call est toujours utilisé pour les structs (parce qu'ils ne peuvent jamais être nuls ou sous-classés).

Modifier En réponse au commentaire de Drew Noakes : Oui, il semble que vous pouvez faire en sorte que le compilateur émette un appel pour n'importe quelle classe, mais seulement dans le cas très spécifique suivant :

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();
    }
}

NOTE La classe ne doit pas nécessairement être scellée pour que cela fonctionne.

Il semble donc que le compilateur émettra un appel si toutes ces choses sont vraies :

  • L'appel de la méthode se fait immédiatement après la création de l'objet
  • La méthode n'est pas implémentée dans une classe de base

0 votes

C'est logique. J'avais étudié le CIL et fait une hypothèse sur le comportement du compilateur. Merci de m'avoir éclairé sur ce point. Je vais mettre à jour ma réponse.

1 votes

En fait, je ne pense pas que vous ayez raison à 100%. J'ai mis à jour mon post (edit 2).

0 votes

Salut Cameron. Pouvez-vous préciser si l'analyse de ce code a été effectuée sur une release build ?

8voto

smwikipedia Points 5491

Selon MSDN :

Appelez :

L'instruction call appelle la méthode indiquée par le descripteur de méthode transmis avec l'instruction. Le descripteur de méthode est un jeton de métadonnées qui indique la méthode à appeler... Le jeton de métadonnées contient suffisamment d'informations pour déterminer si l'appel concerne une méthode statique, une méthode d'instance, une méthode virtuelle ou une fonction globale. Dans tous ces cas, l'adresse de destination est entièrement déterminée à partir du descripteur de méthode. (Comparez cela à l'instruction Callvirt pour appeler des méthodes virtuelles, où l'adresse de destination dépend également du type d'exécution de la référence d'instance poussée avant la Callvirt).

CallVirt :

L'instruction callvirt appelle une méthode tardive sur un objet. C'est-à-dire, la méthode est choisie sur la base du type d'exécution de l'objet plutôt que sur la base de la classe de compilation visible dans le pointeur de la méthode. . Callvirt peut être utilisé pour appeler des méthodes virtuelles et d'instance.

En gros, différentes voies sont empruntées pour invoquer la méthode d'instance d'un objet, qu'elle soit surchargée ou non :

Appel : variable -> de la variable type objet -> méthode

CallVirt : variable -> objet instance -> de l'objet type objet -> méthode

6voto

Boris Points 41

Une chose qui mérite peut-être d'être ajoutée aux réponses précédentes, il semble qu'il n'y ait qu'un seul aspect de l'exécution de "IL call", et deux facettes de l'exécution de "IL callvirt".

Prenez cet exemple de configuration.

    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; }
    }

Tout d'abord, le corps CIL de FInst() et FExt() est 100% identique, opcode à opcode (sauf que l'un est déclaré "instance" et l'autre "statique") -- cependant, FInst() sera appelée avec "callvirt" et FExt() avec "call".

Deuxièmement, FInst() et FVirt() seront tous deux appelés avec "callvirt". -- même si l'un est virtuel et pas l'autre mais ce n'est pas le "même callvirt" qui sera réellement exécuté.

Voici ce qui se passe en gros après le 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 seule différence entre "call" et "callvirt[instance]" est que "callvirt[instance]" essaie intentionnellement d'accéder à un octet de *pObj avant d'appeler le pointeur direct de la fonction d'instance (afin d'éventuellement lever une exception "à cet instant précis").

Ainsi, si vous êtes ennuyé par le nombre de fois où vous devez écrire la "partie vérification" de l'application

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

Vous ne pouvez pas pousser "if (this==null) return SOME_DEFAULT_E ;" dans ClassD.GetE() lui-même (car la sémantique "IL callvirt[instance]" vous interdit de le faire) mais vous êtes libre de le pousser dans .GetE() si vous déplacez .GetE() vers une fonction d'extension quelque part (car la sémantique "IL call" le permet -- mais hélas, vous perdez l'accès aux membres privés, etc.)

Cela dit, l'exécution de "callvirt[instance]" a plus en commun avec "call" qu'avec "callvirt[virtual]", puisque ce dernier peut avoir à exécuter une triple indirection afin de trouver l'adresse de votre fonction. (indirection vers base typedef, puis vers base-vtab-ou-quelque-interface, puis vers slot réel)

J'espère que cela vous aidera, Boris

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X