This post originally appeared on Medium
C# almost always emit “callvirt” for non-virtual method except…
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.
Okay, so 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.
The
callvirt
instruction calls a late-bound method on an object. That is, the method is chosen based on the runtime type ofobj
rather than the compile-time class visible in the method pointer.Callvirt
can be used to call both virtual and instance methods.
Here is the official documentation:
OpCodes.Callvirt Field (System.Reflection.Emit)
And 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 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.
OpCodes.Call Field (System.Reflection.Emit)
Now, let’s change the code a little bit, saving the object to an instance variable.
Uh oh, wait, the generated IL is not call
but callvirt
…
Well, actually it’s by design since .NET v1.
Why does C# always use callvirt?
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 callvirt
.
Now, let’s add virtual.
This time, callvirt
is being used, as expected.
Benchmarks
How about benchmarking callvirt
vs call
?
We need to make sure we really compare callvirt
and 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.
Summary
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.
Top comments (0)