This post originally appeared on Medium
Let’s dive into IL world a little bit.
First, take a look at the following super simple C# code running on LINQPad:
We can see the generated IL from the IL Results Panel, also shown in the above screenshot. Let’s just pay attention to the highlighted parts.
new Coba().Say() will translates to IL call.
Now, let’s add
virtual to the method. Like so:
Oh, now the IL generates
callvirt instead of
call, which makes sense.
callvirtinstruction calls a late-bound method on an object. That is, the method is chosen based on the runtime type of
objrather than the compile-time class visible in the method pointer.
Callvirtcan be used to call both virtual and instance methods.
Here is the official documentation:
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 and the number, type, and order of the arguments that have been placed on the stack to be passed to that method as well as the calling convention to be used.
Now, let’s change the code a little bit, saving the object to an instance variable.
Uh oh, wait, the generated IL is not
Well, actually it’s by design since .NET v1.
C# has evolved since then. C# now has
?. null-conditional operator. Let’s change the code again to use this operator.
Okay, this way the compiler can be sure that the object won’t be null so it can optimize it to use
call instead of
Now, let’s add virtual.
callvirt is being used, as expected.
How about benchmarking
We need to make sure we really compare
call. Let’s make a diff of the last two codes above. Here is the output:
We are good to go.
Here is the code of the benchmark.
And the result:
The result tells us that the performance impact is so small.
C# almost always emit a
callvirt when calling a method, even on a non-virtual method.
callvirt has the advantage of doing an implicit null-check, so that the
NullReferenceException will be thrown when you call a method on a null object. If the compiler ‘knows’ that the object won’t be null, it can optimize away to use
call instead. The performance impact is so small that we should not really care about it.