Wat is het verschil tussen de CIL-instructies “Bellen” en “Callvirt”?
Antwoord 1, autoriteit 100%
Als de runtime een call
-instructie uitvoert, roept het een exact stuk code (methode) aan. Er is geen twijfel over waar het bestaat. Zodra de IL is JITted, is de resulterende machinecode op de oproepsite een onvoorwaardelijke jmp
-instructie.
De instructie callvirt
daarentegen wordt gebruikt om virtuele methoden op een polymorfe manier aan te roepen. De exacte locatie van de code van de methode moet tijdens runtime worden bepaald voor elke aanroep. De resulterende JITted-code omvat enige indirectheid via vtable-structuren. Daarom is de oproep langzamer uit te voeren, maar is hij flexibeler omdat hij polymorfe oproepen mogelijk maakt.
Merk op dat de compiler call
instructies voor virtuele methoden kan uitzenden. Bijvoorbeeld:
sealed class SealedObject : object
{
public override bool Equals(object o)
{
// ...
}
}
Overweeg de oproepcode:
SealedObject a = // ...
object b = // ...
bool equal = a.Equals(b);
Hoewel System.Object.Equals(object)
een virtuele methode is, is er bij dit gebruik geen manier om de Equals
methode te overbelasten. SealedObject
is een verzegelde klasse en kan geen subklassen hebben.
Om deze reden kunnen de sealed
klassen van .NET betere methode-dispatchprestaties hebben dan hun niet-verzegelde tegenhangers.
EDIT:Het bleek dat ik het bij het verkeerde eind had. De C#-compiler kan geen onvoorwaardelijke sprong maken naar de locatie van de methode omdat de referentie van het object (de waarde van this
binnen de methode) null kan zijn. In plaats daarvan zendt het callvirt
uit, dat de null-controle uitvoert en indien nodig gooit.
Dit verklaart eigenlijk een aantal bizarre code die ik in het .NET-framework vond met Reflector:
if (this==null) // ...
Het is mogelijk voor een compiler om verifieerbare code uit te zenden die een null-waarde heeft voor de this
pointer (local0), alleen csc doet dit niet.
Dus ik denk dat call
alleen wordt gebruikt voor statische methoden en structs van klassen.
Gezien deze informatie lijkt het me nu dat sealed
alleen nuttig is voor API-beveiliging. Ik vond een andere vraagdie lijkt te suggereren dat er geen prestatievoordelen zijn je lessen verzegelen.
EDIT 2:Er is meer aan de hand dan het lijkt. De volgende code zendt bijvoorbeeld een call
-instructie uit:
new SealedObject().Equals("Rubber ducky");
In zo’n geval is er natuurlijk geen kans dat de objectinstantie null is.
Interessant is dat in een DEBUG-build de volgende code callvirt
uitzendt:
var o = new SealedObject();
o.Equals("Rubber ducky");
Dit komt omdat je een breekpunt op de tweede regel kunt instellen en de waarde van o
kunt wijzigen. In release-builds stel ik me voor dat de aanroep een call
zou zijn in plaats van callvirt
.
Helaas is mijn pc momenteel buiten werking, maar ik zal hiermee experimenteren zodra hij weer aan de gang is.
Antwoord 2, autoriteit 85%
call
is voor het aanroepen van niet-virtuele, statische of superklasse-methoden, d.w.z. het doel van de aanroep is niet onderhevig aan opheffing. callvirt
is voor het aanroepen van virtuele methoden (zodat als this
een subklasse is die de methode overschrijft, in plaats daarvan de subklasseversie wordt aangeroepen).
Antwoord 3, autoriteit 18%
Om deze reden kunnen de verzegelde klassen van .NET betere prestaties leveren bij het verzenden van methoden dan hun niet-verzegelde tegenhangers.
Helaas is dit niet het geval. Callvirt doet nog iets dat het nuttig maakt. Als een object een methode heeft die wordt aangeroepen, zal callvirt controleren of het object bestaat, en zo niet, dan wordt een NullReferenceException gegenereerd. De oproep springt gewoon naar de geheugenlocatie, zelfs als de objectreferentie er niet is, en probeert de bytes op die locatie uit te voeren.
Wat dit betekent is dat callvirt altijd wordt gebruikt door de C#-compiler (niet zeker over VB) voor klassen, en call wordt altijd gebruikt voor structs (omdat ze nooit null of subklasse kunnen zijn).
BewerkenAls reactie op de opmerking van Drew Noakes: Ja, het lijkt erop dat je de compiler een aanroep voor elke klasse kunt laten uitzenden, maar alleen in het volgende zeer specifieke geval:
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();
}
}
OPMERKINGDe klas hoeft niet verzegeld te zijn om dit te laten werken.
Het lijkt er dus op dat de compiler een aanroep zal uitzenden als al deze dingen waar zijn:
- De methode-aanroep is direct na het maken van het object
- De methode is niet geïmplementeerd in een basisklasse
Antwoord 4, autoriteit 12%
Volgens MSDN:
Bel:
De aanroepinstructie roept de methode aan die wordt aangegeven door de methodedescriptor die bij de instructie is doorgegeven. De methodedescriptor is een metadatatoken dat aangeeft welke methode moet worden aangeroepen… De metadatatoken bevat voldoende informatie om te bepalen of de aanroep een statische methode, een instantiemethode, een virtuele methode of een globale functie is. In al deze gevallen wordt het bestemmingsadres volledig bepaald op basis van de methodedescriptor(contrast dit met de Callvirt-instructie voor het aanroepen van virtuele methoden, waarbij het bestemmingsadres ook afhangt van het runtime-type van de instantieverwijzing die eerder is gepusht de Callvirt).
De callvirt instructie roept een late-bound methode aan op een object. Dat wil zeggen, de methode wordt gekozen op basis van het runtime-type van obj in plaats van de compile-time-klasse die zichtbaar is in de methode-aanwijzer. Callvirt kan worden gebruikt om zowel virtuele als instantiemethoden aan te roepen.
Dus in principe worden verschillende routes gevolgd om de instantiemethode van een object aan te roepen, al dan niet overschreven:
Oproep: variabele -> variabeletype object -> methode
CallVirt: variabele -> objectinstantie -> objecttype object -> methode
Antwoord 5, autoriteit 7%
Eén ding dat misschien de moeite waard is om aan de vorige antwoorden toe te voegen, is:
er lijkt maar één gezicht te zijn voor hoe “IL-oproep” daadwerkelijk wordt uitgevoerd,
en twee gezichten op hoe “IL callvirt” wordt uitgevoerd.
Neem deze voorbeeldconfiguratie.
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; }
}
Ten eerste is de CIL-body van FInst() en FExt() 100% identiek, opcode-naar-opcode
(behalve dat de ene is gedeclareerd als “instantie” en de andere als “statisch”)
— FInst() wordt echter aangeroepen met “callvirt” en FExt() met “call”.
Ten tweede zullen FInst() en FVirt() beide worden aangeroepen met “callvirt”
— ook al is de ene virtueel maar de andere niet —
maar het is niet de “dezelfde callvirt” die echt zal worden uitgevoerd.
Dit is wat er ongeveer gebeurt na 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]
Het enige verschil tussen “call” en “callvirt[instance]” is dat “callvirt[instance]” opzettelijk probeert toegang te krijgen tot één byte van *pObj voordat het de directe aanwijzer van de instantiefunctie aanroept (om mogelijk een uitzondering “daar en dan”).
Dus als je je ergert aan het aantal keren dat je het “controlegedeelte” van
moet schrijven
var d = GetDForABC (a, b, c);
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E;
Je kunt niet op “if (this==null) return SOME_DEFAULT_E;” drukken naar ClassD.GetE() zelf
(aangezien de semantiek van “IL callvirt[instance]” u dit verbiedt)
maar je bent vrij om het in .GetE() te duwen als je .GetE() ergens naar een extensiefunctie verplaatst (zoals de “IL call”-semantiek dit toestaat — maar helaas, de toegang tot privé-leden verliezen enz.)
Dat gezegd hebbende, de uitvoering van “callvirt[instance]” heeft meer gemeen
met “call” dan met “callvirt[virtual]”, aangezien de laatste mogelijk een drievoudige indirecte moet uitvoeren om het adres van uw functie te vinden.
(indirect naar typedef base, dan naar base-vtab-or-some-interface, dan naar daadwerkelijk slot)
Ik hoop dat dit helpt,
Boris
Antwoord 6, autoriteit 2%
Ik voeg alleen maar toe aan de bovenstaande antwoorden, ik denk dat de wijziging al lang geleden is doorgevoerd, zodat Callvirt IL-instructies worden gegenereerd voor alle instance-methoden en Call IL-instructies worden gegenereerd voor statische methoden.
Referentie:
Pluralsight-cursus “C# Language Internals – Part 1 door Bart De Smet (video — Belinstructies en call-stacks in CLR IL in een notendop)
en ook
https://blogs .msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/