4

Why does the compiler generate a callvirt instruction for the call to the explicitly implemented interface method and a call for the call to the implicilty implemented interface method in the following code?

The compiler was mono's mcs 4.2.2 with optimisation turned on.

public interface ITest
{
  void ExplicitInterfaceMethod();
  void ImplicitInterfaceMethod();
}

public sealed class Test : ITest
{
  void ITest.ExplicitInterfaceMethod()
  { }

  public void ImplicitInterfaceMethod()
  { }

  public void InstanceMethod()
  { }

  public void CallTest()
  {
    ((ITest) this).ExplicitInterfaceMethod();
    // IL_0000:  ldarg.0 
    // IL_0001:  callvirt instance void class ITest::ExplicitInterfaceMethod()

    this.ImplicitInterfaceMethod();
    // IL_0006:  ldarg.0 
    // IL_0007:  call instance void class Test::ImplicitInterfaceMethod()

    InstanceMethod();
    // IL_000c:  ldarg.0 
    // IL_000d:  call instance void class Test::InstanceMethod()
  }
}

What I found out so far:

  • callvirt is used on a "nullable receiver" because it does a null check before emitting a jump to the method. It seems that this may be null. (Call and Callvirt)
  • call is used if the compiler can prove that the receiver is non-null.
  • optimisations turned off may produce more callvirts to help the debugger. (Hence I compiled with optimisations turned on.)

In this case, it seems to me that this is always non-null because otherwise we would not have ended up in the enclosing method anyway.

Does mono miss an optimisation here? Or is there a possibility for this to become null?

I could imagine such situations if finalizers were involved somehow, but this is not the case here. And if it would be possible for this to become null here, then wouldn't it be wrong to use call at all?

EDIT

From the answer of @jonathon-chase and the comments to the question I distilled a working theory for now: methods on the interface must be virtual because you cannot, in general, statically determine if the implementing type provides an 'ordinary' or a virtual/abstract implementation. To make sure that virtual methods on the implementing type hierarchy work when called via an interface callvirt is the way to go. (See my comment to the question on calling the implicit method via the interface).

Regarding the potential optimisation:

In my example I have a sealed type and I only call inside my own inheritance hierarchy. The compiler could statically determine that 1) the implementation is non-virtual, 2) it is invoked on the this reference, and 3) the hierarchy is bounded because of the sealed keyword; so there is no way for a virtual implementation to exist. I think it would be possible to use call in this case, but I also see that the benefits are neglectible compared to the amound of work this analysis would need.

cpt. jazz
  • 1,336
  • 12
  • 21
  • What happens if you use `((ITest) this).ImplicitInterfaceMethod();`? – Grax32 Jan 23 '16 at 23:17
  • @Grax Then this call also becomes a ``callvirt``: ``IL_0007: callvirt instance void class ITest::ImplicitInterfaceMethod()`` – cpt. jazz Jan 23 '16 at 23:20
  • Same `callvirt` with Visual Studio 2015. It seems like a corner case where the compiler possibly could optimize here, but we are talking about nanoseconds here? – Jesse Good Jan 23 '16 at 23:38
  • @JesseGood I know that the performance impact is almost zero, but it seems inconsistent to me. The cast cannot produce ``null`` because it would throw if it cannot coerce the instance to the desired type. So ``callvirt`` seems like an overhead to me. And the compiler was smart enough to emit a ``call`` for the other methods (which it would not have done if we called it like ``someInstance.InstanceMethod()``.) – cpt. jazz Jan 23 '16 at 23:48
  • call vs callvirt in this case has to do with how you are accessing the method. The difference is not due to an extra optimization. Accessing via an interface(ITest.Method) emits callvirt while accessing via a concrete type(Test.Method) emits call. It's true that the compiler could likely optimize the explicit interface method calls. However it's probably easier to leave that up to the JIT. Which in this case the microsoft JIT emits the same x86 for both implicit/explicit calls. – Will Jan 24 '16 at 02:37

1 Answers1

3

It looks like interface methods are implemented as virtual, and so the explicit implementations are overriden virtual method implementations. The more I think about this, the more it seems to make sense that an explicit implementation is really a virtual overload.

I haven't checked with the mono compiler just yet, but here's a dump from ildasm.exe after using /target:library /optimize+ in csc. As you can see, the interface method is virtual where declared on the interface. When casting the type to the interface, it seems to make sense that we are providing a virtual overload for that method, instead of the implicitly declared method on the same class. Would still love a more knowledgeable person than I to weigh in.

Code used:

using System;

public interface ITest
{
  void TestMethod();
}

public class Test : ITest
{
  void ITest.TestMethod()
  {
    Console.WriteLine("I am Test");
  }

  void TestMethod()
  {
    Console.WriteLine("I am other test");
  }
}

IL output:

.class interface public abstract auto ansi ITest
{
  .method public hidebysig newslot abstract virtual 
          instance void  TestMethod() cil managed
  {
  } // end of method ITest::TestMethod

} // end of class ITest

.class public auto ansi beforefieldinit Test
       extends [mscorlib]System.Object
       implements ITest
{
  .method private hidebysig newslot virtual final 
          instance void  ITest.TestMethod() cil managed
  {
    .override ITest::TestMethod
    // Code size       11 (0xb)
    .maxstack  8
    IL_0000:  ldstr      "I am Test"
    IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000a:  ret
  } // end of method Test::ITest.TestMethod

  .method private hidebysig instance void 
          TestMethod() cil managed
  {
    // Code size       11 (0xb)
    .maxstack  8
    IL_0000:  ldstr      "I am other test"
    IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000a:  ret
  } // end of method Test::TestMethod
Jonathon Chase
  • 9,396
  • 21
  • 39
  • I think it now makes sense to me. My working theory for now: methods on the interface _must_ be virtual because you cannot statically determine if the implementing type provides an 'ordinary' or a virtual/abstract implementation. To make sure that virtual methods on the implementing type hierarchy work when called via an interface ``callvirt`` is the way to go. (Also see my comment to the question on calling the implicit method via the interface). – cpt. jazz Jan 24 '16 at 17:38
  • Right. I think the JIT is probably going to be smart enough to optimize the execution once it translates from IL to byte-code, but that's me making assumptions way outside of my knowledge-base. – Jonathon Chase Jan 24 '16 at 18:11