Was ist der Unterschied zwischen den CIL-Anweisungen "Call" und "Callvirt"?Call and Callvirt
Antwort
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).
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.
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
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
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. –
Eigentlich glaube ich nicht, dass Sie hier zu 100% korrekt sind. Ich habe meinen Beitrag aktualisiert (2). –
Hallo Cameron. Können Sie klären, ob die Analyse dieses Codes bei einem Release-Build durchgeführt wurde? –
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).
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
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
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/
'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)). –
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
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). –
Sie könnten den Leistungsunterschied in Ihrer Antwort erwähnen, der der Grund dafür ist, eine 'Anruf' Anweisung überhaupt zu haben. –