2008-10-11 16 views

Antwort

42

call ist für den Aufruf nicht virtueller, statischer oder superclass-Methoden, d. H. Das Ziel des Aufrufs unterliegt nicht dem Überschreiben. callvirt ist für den Aufruf virtueller Methoden (wenn also this eine Unterklasse ist, die die Methode überschreibt, wird stattdessen die Unterklassenversion aufgerufen).

+25

Wenn ich mich richtig erinnere, überprüft "call" den Zeiger nicht vor dem Aufruf auf null, was "callvirt" offensichtlich benötigt. Deshalb wird "callvirt" manchmal vom Compiler ausgegeben, obwohl nicht-virtuelle Methoden aufgerufen werden. – dalle

+1

Ah, danke, dass du das gezeigt hast (ich bin kein. NET-Person). Die Analogien, die ich verwende, sind Call => Invokespecial und Callvirt => Invokevirtual, in JVM Bytecode. Im Fall der JVM überprüfen beide Anweisungen "dies" auf Nullheit (ich habe gerade ein Testprogramm geschrieben, um es zu überprüfen). –

+2

Sie könnten den Leistungsunterschied in Ihrer Antwort erwähnen, der der Grund dafür ist, eine 'Anruf' Anweisung überhaupt zu haben. –

45

Wenn die Laufzeit eine call Anweisung ausführt, ruft sie einen genauen Codeabschnitt (Methode) auf. Es gibt keine Frage, wo es existiert. Sobald die IL JITtated ist, ist der resultierende Maschinencode an der Aufrufstelle eine unbedingte jmp Anweisung.

Im Gegensatz dazu wird die Anweisung callvirt verwendet, um virtuelle Methoden in einer polymorphen Weise aufzurufen. Die genaue Position des Methodencodes muss zur Laufzeit für jeden Aufruf festgelegt werden. Der resultierende JIT-Code beinhaltet eine gewisse Indirektion durch vtable-Strukturen. Daher ist der Aufruf langsamer auszuführen, aber er ist flexibler, da er polymorphe Aufrufe ermöglicht.

Beachten Sie, dass der Compiler call Anweisungen für virtuelle Methoden ausgeben kann. Zum Beispiel:

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

Telefonvorwahl Bedenken Sie:

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

bool equal = a.Equals(b); 

Während System.Object.Equals(object) eine virtuelle Methode ist, in dieser Verwendung gibt es keine Möglichkeit für eine Überlastung des Equals Verfahren zu existieren. SealedObject ist eine versiegelte Klasse und kann keine Unterklassen haben.

Aus diesem Grund können die Klassen sealed von .NET eine bessere Methoden-Dispatching-Leistung als ihre nicht versiegelten Gegenstücke haben.

EDIT: Stellt sich heraus, dass ich falsch lag. Der C# -Compiler kann keinen unbedingten Sprung zum Speicherort der Methode ausführen, da die Referenz des Objekts (der Wert this innerhalb der Methode) null sein kann. Stattdessen gibt es callvirt aus, die den Null-Check durchführt und bei Bedarf wirft.

Das eigentlich erklärt einige bizarre Code, den ich in .NET Framework Reflector gefunden:

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

Es ist möglich, dass ein Compiler überprüfbaren Code zu emittieren, die für den this Zeiger (local0) einen Nullwert hat, nur CSC macht das nicht.

Also ich denke, call wird nur für statische Methoden und Strukturen der Klasse verwendet.

Angesichts dieser Informationen scheint mir jetzt, dass sealed nur für API-Sicherheit nützlich ist. Ich habe another question gefunden, was darauf hindeutet, dass es keine Leistungsvorteile beim Versiegeln Ihrer Klassen gibt.

EDIT 2: Es gibt mehr als es scheint. Zum Beispiel gibt der folgende Code eine call Anweisung:

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

Offensichtlich in einem solchen Fall gibt es keine Chance, dass das Objekt Instanz null sein könnte.

Interessanterweise wird in einer Debug-Build, gibt den folgenden Code callvirt:

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

Dies ist, weil Sie einen Haltepunkt in der zweiten Zeile setzen konnten und den Wert von o ändern. In Release-Builds stelle ich mir vor, der Anruf wäre ein call anstatt callvirt.

Leider ist mein PC derzeit außer Betrieb, aber ich werde mit diesem experimentieren, sobald es wieder da ist.

+2

Versiegelte Attribute sind definitiv schneller, wenn sie nach Reflexion suchen, aber ansonsten I Ich kenne keine anderen Vorteile, die Sie nicht erwähnt haben. – TraumaPony

11

Aus diesem Grund können die versiegelten Klassen von .NET eine bessere Methodentransportleistung haben als ihre nicht versiegelten Gegenstücke.

Leider ist dies nicht der Fall. Callvirt macht noch eine andere Sache, die es nützlich macht. Wenn für ein Objekt eine Methode aufgerufen wird, prüft callvirt, ob das Objekt vorhanden ist, und falls nicht, wird eine NullReferenceException ausgelöst. Call wird einfach zum Speicherort springen, auch wenn die Objektreferenz nicht da ist, und versuchen, die Bytes an diesem Ort auszuführen. Das bedeutet, dass callvirt immer vom C# -Compiler (nicht sicher über VB) für Klassen verwendet wird und call immer für Strukturen verwendet wird (weil sie niemals null oder unterklassifiziert sein können).

bearbeitet Als Antwort auf Drew Noakes Kommentar: Ja, es scheint, dass Sie den Compiler bekommen können einen Anruf für jede Klasse zu emittieren, sondern nur in dem folgenden sehr speziellen Fall:

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

HINWEIS Der Klasse muss nicht versiegelt werden, damit dies funktioniert.

So sieht es aus wie der Compiler einen Anruf aussendet, wenn all diese Dinge sind wahr:

  • Die Call-Methode ist unmittelbar nach der Objekterstellung
  • Verfahren nicht in einer Basisklasse implementiert wird
+0

Das macht Sinn. Ich hatte CIL studiert und eine Annahme über das Verhalten des Compilers gemacht. Danke für die Klärung. Ich werde meine Antwort aktualisieren. –

+1

Eigentlich glaube ich nicht, dass Sie hier zu 100% korrekt sind. Ich habe meinen Beitrag aktualisiert (2). –

+0

Hallo Cameron. Können Sie klären, ob die Analyse dieses Codes bei einem Release-Build durchgeführt wurde? –

5

Laut MSDN:

Call:

Die Aufrufanweisung ruft die Methode auf, die durch den mit der Anweisung übergebenen Methodendeskriptor angegeben wurde. Der Methodendeskriptor ist ein Metadatentoken, der die aufzurufende Methode angibt. Das Metadatentoken enthält ausreichende Informationen, um zu ermitteln, ob der Aufruf einer statischen Methode, einer Instanzmethode, einer virtuellen Methode oder einer globalen Funktion entspricht. In allen diesen Fällen wird die Zieladresse vollständig aus dem Methodendeskriptor bestimmt (im Gegensatz dazu die Callvirt-Anweisung zum Aufrufen virtueller Methoden, wobei die Zieladresse auch vom Laufzeittyp der Instanzreferenz vor dem Callvirt abhängt).

CallVirt:

Die Anweisung callvirt ruft eine spät gebundene Methode für ein Objekt. Das heißt, Die Methode wird basierend auf dem Laufzeittyp von obj statt der Kompilierzeitklasse ausgewählt, die im Methodenzeiger sichtbar ist.Callvirt kann verwendet werden, um sowohl virtuelle als auch Instanzmethoden aufzurufen.

Also im Grunde verschiedene Routen genommen eine Objektinstanzmethode, außer Kraft gesetzt oder nicht aufzurufen:

Call: Variable ->Variablen Typ Objekt -> Methode

callvirt: Variable -> Objektinstanz ->Objekt Typ Objekt -> Methode

2

eine Sache, vielleicht wert zusätzlich zu den bisherigen Antworten ist, es scheint nur eine Seite zu sein, wie „IL nennen“ tatsächlich ausführt, und zwei Gesichter wie "IL callvirt" ausgeführt wird.

Nehmen Sie diese Beispielkonfiguration.

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

Zuerst wird der CIL Körper Finst() und FEXT() ist zu 100% identisch, opcode-to-Opcode (außer, dass eine "Instanz" und die andere "static" deklariert ist) - FInst() wird jedoch mit "callvirt" und FExt() mit "call" aufgerufen.

Zweitens Finst() und fVirt() werden beide mit „callvirt“ genannt werden - auch wenn man virtuell ist aber das andere nicht - aber es ist nicht das „gleiche callvirt“, die wirklich bekommen ausführen.

Hier ist, was passiert, etwa nach 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] 

Der einzige Unterschied zwischen „Call“ und „callvirt [Instanz]“ ist, dass „callvirt [Instanz]“ absichtlich ein Byte von * pObj zuzugreifen versucht vor es ruft den direkten Zeiger der Instanzfunktion auf (um möglicherweise eine Ausnahme "genau dort und dann" zu werfen).

Wenn Sie also von der Anzahl, wie oft verärgert sind, dass Sie die „Überprüfung Teil“ des

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

Sie können nicht schieben schreiben „wenn (diese == null) return SOME_DEFAULT_E;“ nach unten in ClassD.GetE() selbst (wie die "IL callvirt [instance]" - Semantik Ihnen dies verbietet) aber Sie können es in .GetE() schieben, wenn Sie .GetE() auf eine Erweiterung verschieben Funktion irgendwo (wie die "IL Call" Semantik erlaubt es - aber leider, den Zugriff auf private Mitglieder usw. zu verlieren)

Das heißt, die Ausführung von "CallVirt [Instanz]" hat mehr gemeinsam mit "Anruf" als mit "callvirt [virtual]", da letzterer möglicherweise eine dreifache Indirektion ausführen muss, um die Adresse Ihrer Funktion zu finden. (Indirektion zu typedef Basis, dann zur Basis-VTAB-or-some-Schnittstelle, dann auf tatsächliche Slot)

hoffe, das hilft, Boris

1

nur auf die oben genannten Antworten geben, ich glaube, die Veränderung hat wurde so lange zurückgestellt, dass der Callvirt-IL-Befehl für alle Instanzmethoden generiert wird und der Call-IL-Befehl für statische Methoden generiert wird.

Referenz:

Plural Kurs „C# Internals - Teil 1 von Bart De Smet (Video - Anweisungen Rufen und Aufruf-Stacks in CLR IL in a Nutshell)

und auch https://blogs.msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/

+0

'Call' wird auch für Base Calls verwendet. Außerdem glaube ich, dass der Roslyn-Compiler Aufrufanweisungen für versiegelte/nicht-virtuelle Methoden generieren wird, wenn er trivialerweise feststellen kann, dass das Objekt niemals null ist (zB [Aufrufe, die vom nullbedingten Operator generiert werden] (http: // stackoverflow.com/questions/34535000/call-instead-of-callvirt-in-case-of-the-new-c-sharp-6-null-check)). –