C#
.NET
programming
call method
virtual functions

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#:

csharp
1public class Base
2{
3    public void NonVirtual() => Console.WriteLine("Base.NonVirtual");
4}

In IL, a direct instance call can look like this:

il
IL_0000: ldarg.0
IL_0001: call instance void Base::NonVirtual()
IL_0006: ret

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.

csharp
1public class Demo
2{
3    public void Print() => Console.WriteLine("print");
4}
5
6Demo demo = new Demo();
7demo.Print();

The generated IL commonly uses callvirt even though Print is not virtual.

il
1IL_0000: newobj instance void Demo::.ctor()
2IL_0005: stloc.0
3IL_0006: ldloc.0
4IL_0007: callvirt instance void Demo::Print()
5IL_000C: ret

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:

csharp
Demo? demo = null;
demo.Print();

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:

  • 'callvirt gives the language a predictable null-checking behavior'
  • 'call is a more direct instruction with fewer dispatch semantics'

Virtual Dispatch Example

Here is the classic polymorphic case:

csharp
1public class Base
2{
3    public virtual void Speak() => Console.WriteLine("Base");
4}
5
6public class Derived : Base
7{
8    public override void Speak() => Console.WriteLine("Derived");
9}
10
11Base value = new Derived();
12value.Speak();

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.

csharp
1public class Derived : Base
2{
3    public override void Speak()
4    {
5        base.Speak();
6        Console.WriteLine("Derived");
7    }
8}

That base call is not polymorphic.

When You Will See Each Instruction

You will commonly see:

  • 'call for static methods and constructors'
  • 'call for explicit base calls'
  • 'callvirt for most instance-method calls from C#'
  • 'callvirt for 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 callvirt only appears for methods declared virtual.
  • Forgetting that C# often uses callvirt for null-check semantics.
  • Treating call as 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

  • 'call performs a direct method invocation.'
  • 'callvirt supports virtual dispatch and also enforces instance null checking.'
  • C# frequently emits callvirt for ordinary instance methods.
  • Base calls and static calls typically use call.
  • When analyzing IL, focus on semantics first and micro-optimization claims second.

Course illustration
Course illustration

All Rights Reserved.