C# generates virtual calls to non-virtual methods as well

Hyderabad Microsoft Campus

Sometime back I had posted about a case where non-virtual calls are used for virtual methods and promised posting about the reverse scenario. This issue of C# generating callvirt IL instruction even for non-virtual method calls keeps coming back on C# discussion DLs every couple of months. So here it goes :)

Consider the following code

 class B
{
    public virtual void Virt(){
        Console.WriteLine("Base::Virt");
    }

    public void Stat(){
        Console.WriteLine("Base::Stat");
    }
}

class D : B
{
    public override void Virt(){
        Console.WriteLine("Derived::Virt");
    }
}

class Program
{
    static void Main(string[] args)
    {
        D d = new D();
        d.Stat(); // should emit the call IL instruction
        d.Virt(); // should emit the callvirt IL instruction
    }
}

The basic scenario is that a base class defines a virtual method and a non-virtual method. A call is made to base using a derived class pointer. The expectation is that the call to the virtual method (B.Virt) will be through the intermediate language (IL) callvirt instruction and that to the non-virtual method (B.Stat) through call IL instruction.

However, this is not true and callvirt is used for both. If we open the disassembly for the Main method using reflector or ILDASM this is what we see

     L_0000: nop 
    L_0001: newobj instance void ConsoleApplication1.D::.ctor()
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: callvirt instance void ConsoleApplication1.B::Stat()
    L_000d: nop 
    L_000e: ldloc.0 
    L_000f: callvirt instance void ConsoleApplication1.B::Virt()
    L_0014: nop 
    L_0015: ret 

Question is why? There are two reasons that have been brought forward by the CLR team

  1. API change.
    The reason is that .NET team wanted a change in an method (API) from non-virtual to virtual to be non-breaking. So in effect since the call is anyway generated as callvirt a caller need not be recompiled in case the callee changes to be a virtual method.

  2. Null checking
    If a call is generated and the method body doesn't access any instance variable then it is possible to even call on null objects successfully. This is currently possible in C++, see a post I made on this here.

    With callvirt there's a forced access to this pointer and hence the object on which the method is being called is automatically checked for null.

callvirt does come with additional performance cost but measurement showed that there's no significant performance difference between call with null check vs callvirt. Moreover, since the Jitter has full metadata of the callee, while jitting the callvirt it can generate processor instructions to do static call if it figures out that the callee is indeed non-virtual.

However, the compiler does try to optimize situations where it knows for sure that the target object cannot be null. E.g. for the expression i.ToString(); where i is an int call is used to call the ToString method because Int32 is value type (cannot be null) and sealed.