Call and Callvirt
Asked Answered
L

6

65

What is the difference between the CIL instructions "Call" and "Callvirt"?

Lightfingered answered 11/10, 2008 at 10:26 Comment(0)
F
54

call is for calling non-virtual, static, or superclass methods, i.e., the target of the call is not subject to overriding. callvirt is for calling virtual methods (so that if this is a subclass that overrides the method, the subclass version is called instead).

Filibeg answered 11/10, 2008 at 10:45 Comment(7)
If I remember correctly call doesn't check the pointer against null before performing the call, something which callvirt obviously needs to. That is why callvirt sometimes is emitted by the compiler even though calling non-virtual methods.Rollins
Ah. Thanks for pointing that out (I'm not a .NET person). The analogies I use are call => invokespecial, and callvirt => invokevirtual, in JVM bytecode. In the case of the JVM, both instructions check "this" for nullness (I just wrote a test program to check).Filibeg
You might want to mention the performance difference in your answer, that being the reason for having a 'call' instruction at all.Tonsorial
I disagree. Not having a performance difference would not eliminate the raison d'etre of the call (or invokespecial) instruction.Filibeg
@ChrisJester-Young - You are wrong. Its always calvirt. Please read here - blogs.msdn.com/b/ericgu/archive/2008/07/02/…Sumptuary
@AngshumanAgarwal Fair enough re C# always using callvirt. Regardless, my description of call and callvirt are (to my understanding) still correct, and my comment about how JVM bytecode works still holds---Java code that uses, say, super.foobar() would indeed still use invokespecial (and not invokevirtual).Filibeg
The call instruction is actually emitted from time to time. From the ILDasm of a test C# program: IL_0021: callvirt instance void [ClassLibrary1]ClassLibrary1.Class1::sayHello() IL_0026: ldstr "This was expected behavior. Will exit with code 100." IL_002b: *call* void [mscorlib]System.Console::WriteLine(string) IL_0030: ldc.i4.s 100Philhellene
B
62

When the runtime executes a call instruction it's making a call to an exact piece of code (method). There's no question about where it exists. Once the IL has been JITted, the resulting machine code at the call site is an unconditional jmp instruction.

By contrast, the callvirt instruction is used to call virtual methods in a polymorphic way. The exact location of the method's code must be determined at runtime for each and every invocation. The resulting JITted code involves some indirection through vtable structures. Hence the call is slower to execute, but it is more flexible in that it allows for polymorphic calls.

Note that the compiler can emit call instructions for virtual methods. For example:

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

Consider calling code:

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

bool equal = a.Equals(b);

While System.Object.Equals(object) is a virtual method, in this usage there is no way for an overload of the Equals method to exist. SealedObject is a sealed class and cannot have subclasses.

For this reason, .NET's sealed classes can have better method dispatching performance than their non-sealed counterparts.

EDIT: Turns out I was wrong. The C# compiler cannot make an unconditional jump to the method's location because the object's reference (the value of this within the method) might be null. Instead it emits callvirt which does the null check and throws if required.

This actually explains some bizarre code I found in the .NET framework using Reflector:

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

It's possible for a compiler to emit verifiable code that has a null value for the this pointer (local0), only csc doesn't do this.

So I guess call is only used for class static methods and structs.

Given this information it now seems to me that sealed is only useful for API security. I found another question that seems to suggest there are no performance benefits to sealing your classes.

EDIT 2: There's more to this than it seems. For example the following code emits a call instruction:

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

Obviously in such a case there is no chance that the object instance could be null.

Interestingly, in a DEBUG build, the following code emits callvirt:

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

This is because you could set a breakpoint on the second line and modify the value of o. In release builds I imagine the call would be a call rather than callvirt.

Unfortunately my PC is currently out of action, but I'll experiment with this once it's up again.

Blocky answered 11/10, 2008 at 10:51 Comment(1)
Sealed attributes are definitely faster when looking for them via reflection, but other than that, I don't know of any other benefits you haven't mentioned.Labarbera
F
54

call is for calling non-virtual, static, or superclass methods, i.e., the target of the call is not subject to overriding. callvirt is for calling virtual methods (so that if this is a subclass that overrides the method, the subclass version is called instead).

Filibeg answered 11/10, 2008 at 10:45 Comment(7)
If I remember correctly call doesn't check the pointer against null before performing the call, something which callvirt obviously needs to. That is why callvirt sometimes is emitted by the compiler even though calling non-virtual methods.Rollins
Ah. Thanks for pointing that out (I'm not a .NET person). The analogies I use are call => invokespecial, and callvirt => invokevirtual, in JVM bytecode. In the case of the JVM, both instructions check "this" for nullness (I just wrote a test program to check).Filibeg
You might want to mention the performance difference in your answer, that being the reason for having a 'call' instruction at all.Tonsorial
I disagree. Not having a performance difference would not eliminate the raison d'etre of the call (or invokespecial) instruction.Filibeg
@ChrisJester-Young - You are wrong. Its always calvirt. Please read here - blogs.msdn.com/b/ericgu/archive/2008/07/02/…Sumptuary
@AngshumanAgarwal Fair enough re C# always using callvirt. Regardless, my description of call and callvirt are (to my understanding) still correct, and my comment about how JVM bytecode works still holds---Java code that uses, say, super.foobar() would indeed still use invokespecial (and not invokevirtual).Filibeg
The call instruction is actually emitted from time to time. From the ILDasm of a test C# program: IL_0021: callvirt instance void [ClassLibrary1]ClassLibrary1.Class1::sayHello() IL_0026: ldstr "This was expected behavior. Will exit with code 100." IL_002b: *call* void [mscorlib]System.Console::WriteLine(string) IL_0030: ldc.i4.s 100Philhellene
Y
13

For this reason, .NET's sealed classes can have better method dispatching performance than their non-sealed counterparts.

Unfortunately this is not the case. Callvirt does one other thing that makes it useful. When an object has a method called on it callvirt will check if the object exists, and if not throws a NullReferenceException. Call will simply jump to the memory location even if the object reference is not there, and try to execute the bytes in that location.

What this means is that callvirt is always used by the C# compiler (not sure about VB) for classes, and call is always used for structs (because they can never be null or subclassed).

Edit In response to Drew Noakes comment: Yes it seems you can get the compiler to emit a call for any class, but only in the following very specific case:

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 The class does not have to be sealed for this to work.

So it looks like the compiler will emit a call if all these things are true:

  • The method call is immediately after the object creation
  • The method is not implemented in a base class
Yacano answered 11/10, 2008 at 11:2 Comment(5)
That makes sense. I had studied CIL and made an assumption about the behaviour of the compiler. Thanks for clearing this up. I'll update my answer.Blocky
Actually I don't believe you're 100% correct here. I've updated my post (edit 2).Blocky
Hi Cameron. Can you clarify whether the analysis of this code was performed on a release build?Blocky
There's no difference between calls in the debug and release builds. The only difference is in the stack manipulation code between calls. CallVirt is used in the first call because it's not calling SampleClass.Equals but Object.Equals.Yacano
Just to point that the compiler will also emit call if the method is static.Renaud
C
8

According to MSDN:

Call:

The call instruction calls the method indicated by the method descriptor passed with the instruction. The method descriptor is a metadata token that indicates the method to call...The metadata token carries sufficient information to determine whether the call is to a static method, an instance method, a virtual method, or a global function. In all of these cases the destination address is determined entirely from the method descriptor (contrast this with the Callvirt instruction for calling virtual methods, where the destination address also depends upon the runtime type of the instance reference pushed before the Callvirt).

CallVirt:

The callvirt instruction calls a late-bound method on an object. That is, the method is chosen based on the runtime type of obj rather than the compile-time class visible in the method pointer. Callvirt can be used to call both virtual and instance methods.

So basically, different routes are taken to invoke an object's instance method, overriden or not:

Call: variable -> variable's type object -> method

CallVirt: variable -> object instance -> object's type object -> method

Chesney answered 3/10, 2010 at 13:34 Comment(0)
G
6

One thing perhaps worth adding to the previous answers is, there seems to be only one face to how "IL call" actually executes, and two faces to how "IL callvirt" executes.

Take this sample setup.

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

First, the CIL body of FInst() and FExt() is 100% identical, opcode-to-opcode (except that one is declared "instance" and the other "static") -- however, FInst() will get called with "callvirt" and FExt() with "call".

Second, FInst() and FVirt() will both be called with "callvirt" -- even though one is virtual but the other isn't -- but it's not the "same callvirt" that will really get to execute.

Here's what roughly happens after 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]  

The only difference between "call" and "callvirt[instance]" is that "callvirt[instance]" intentionally tries to access one byte from *pObj before it calls the direct pointer of the instance function (in order to possibly throw an exception "right there and then").

Thus, if you're annoyed by the number of times that you have to write the "checking part" of

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

You cannot push "if (this==null) return SOME_DEFAULT_E;" down into ClassD.GetE() itself (as the "IL callvirt[instance]" semantics prohibits you to do this) but you're free to push it into .GetE() if you move .GetE() to an extension function somewhere (as the "IL call" semantic allows it -- but alas, losing access to private members etc.)

That said, the execution of "callvirt[instance]" has more in common with "call" than with "callvirt[virtual]", since the latter may have to execute a triple indirection in order to find the address of your function. (indirection to typedef base, then to base-vtab-or-some-interface, then to actual slot)

Hope this helps, Boris

Gob answered 28/6, 2015 at 15:15 Comment(0)
L
1

Just adding to the above answers, I think the change has been made long back such that Callvirt IL instruction will get generated for all the instance methods and Call IL instruction will get generated for static methods.

Reference :

Pluralsight course "C# Language Internals - Part 1 by Bart De Smet (video -- Call instructions and call stacks in CLR IL in a Nutshell)

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

Laniary answered 13/3, 2017 at 16:1 Comment(1)
call also gets used for base calls. Also, I believe that the roslyn compiler will generate call instructions for sealed/non-virtual methods if it can trivially determine that the object will never be null (eg. calls generated from the null conditional operator).Salvatoresalvay

© 2022 - 2024 — McMap. All rights reserved.