What is the difference between the CIL instructions "Call" and "Callvirt"?
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.