Call and Callvirt
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
call and callvirt are Common Intermediate Language instructions, and they do not mean exactly "non-virtual" and "virtual" in the simplistic way many summaries claim. The real difference is about dispatch behavior and null checking, and that is why C# often emits callvirt even for methods that are not declared virtual.
What call Does
call invokes the target method directly. There is no virtual dispatch lookup through the runtime type.
That makes call appropriate for:
- static methods
- constructors
- base-class method calls
- non-virtual calls where direct dispatch is desired
A simple example in C#:
In IL, a direct instance call can look like this:
The runtime does not perform virtual method dispatch here.
What callvirt Does
callvirt performs virtual dispatch when the target method is virtual. It also performs a null check on the instance reference before making the call.
That second point is important. Even when the target method is not virtual, compilers often emit callvirt so that invoking an instance method on null throws NullReferenceException before entering the target method body.
The generated IL commonly uses callvirt even though Print is not virtual.
That is normal C# output, not a compiler bug.
Why C# Prefers callvirt for Instance Methods
The usual reason is null-reference semantics. Consider this code:
From the language point of view, that should throw when the method is invoked. Emitting callvirt guarantees the instance reference is checked before control enters the method.
If raw IL used call against an instance method on a null reference, behavior depends on what the method body does with this. The call instruction itself does not provide the same front-loaded null-check guarantee.
So the simplified rule is:
- '
callvirtgives the language a predictable null-checking behavior' - '
callis a more direct instruction with fewer dispatch semantics'
Virtual Dispatch Example
Here is the classic polymorphic case:
This must dispatch to Derived.Speak(), so IL uses virtual call semantics.
If you instead write a base-qualified call from inside Derived, the compiler can emit a direct call to the base implementation because the dispatch target is fixed.
That base call is not polymorphic.
When You Will See Each Instruction
You will commonly see:
- '
callfor static methods and constructors' - '
callfor explicit base calls' - '
callvirtfor most instance-method calls from C#' - '
callvirtfor polymorphic virtual dispatch'
The important lesson is that callvirt is not restricted to methods declared with the virtual keyword.
Performance Notes
Do not try to outsmart the JIT based on raw IL mnemonics alone. Modern .NET runtimes can devirtualize calls, inline methods, and optimize based on actual type information.
If performance matters, measure the compiled code path instead of assuming call is always materially faster than callvirt. The semantic difference is more important than the superficial instruction name.
Common Pitfalls
- Assuming
callvirtonly appears for methods declaredvirtual. - Forgetting that C# often uses
callvirtfor null-check semantics. - Treating
callas universally "faster" without measuring actual JIT output. - Confusing base-method calls with polymorphic dispatch.
- Reading IL literally without considering what the source language guarantees.
Summary
- '
callperforms a direct method invocation.' - '
callvirtsupports virtual dispatch and also enforces instance null checking.' - C# frequently emits
callvirtfor ordinary instance methods. - Base calls and static calls typically use
call. - When analyzing IL, focus on semantics first and micro-optimization claims second.

