为什么显式实现接口方法需要使用callvirt而隐式实现不需要?

4
为什么编译器在以下代码中对显式实现接口的方法调用生成一个callvirt指令以及对隐式实现接口的方法调用生成一个call呢?此代码使用的编译器是带有优化功能的mono's mcs 4.2.2版本。
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()
  }
}

目前我所了解的:

  • callvirt 在“可空接收器”上使用,因为它在发出跳转到方法之前进行了空检查。看起来 this 可能为 null。(Call and Callvirt
  • call 被用于编译器可以证明接收器非空的情况。
  • 关闭优化可能会产生更多的 callvirt 以帮助调试器。(因此我打开了优化选项进行编译。)

在这种情况下,对我来说,this 总是非空的,否则我们不会进入封闭的方法。

mono 是否错过了一个优化?或者 this 有可能变成 null 吗?

我可以想象出某些情况,如果涉及到终结器,但这在这里并不是问题。而且,如果在这里 this 可能为空,那么使用 call 就是错误的吗?

编辑

从 @jonathon-chase 的答案和问题的评论中,我总结出了一个暂时可行的理论:接口上的方法必须是虚拟的,因为你不能静态地确定实现类型是否提供了“普通”或虚拟/抽象实现。为确保在通过接口调用时实现类型层次结构上的虚拟方法正常工作,callvirt 是正确的方式。(请参见我对通过接口调用隐式方法的问题的评论。)

关于潜在的优化:

在我的例子中,我有一个 sealed 类型,并且我只在自己的继承层次结构内进行调用。编译器可以静态地确定:1)实现是非虚拟的,2)它在 this 引用上被调用,3)由于 sealed 关键字,层次结构是有限制的,因此不存在虚拟实现的可能性。我认为在这种情况下可以使用 call,但我也看到与需要进行的分析相比,好处微不足道。


如果使用 ((ITest) this).ImplicitInterfaceMethod(); 会发生什么? - Grax32
@Grax 那么这个调用也变成了“callvirt”:“IL_0007: callvirt instance void class ITest::ImplicitInterfaceMethod()” - cpt. jazz
使用Visual Studio 2015相同的“callvirt”。这似乎是编译器可能会优化的特殊情况,但我们在谈论纳秒吗? - Jesse Good
@JesseGood 我知道性能影响几乎为零,但它对我来说似乎不一致。强制转换不能产生“null”,因为如果无法将实例强制转换为所需类型,则会抛出异常。因此,“callvirt”对我来说似乎是一个开销。而编译器足够聪明,可以为其他方法发出“call”(如果我们像“someInstance.InstanceMethod()”那样调用它,则不会这样做)。 - cpt. jazz
在这种情况下,call和callvirt涉及您如何访问方法。差异不是由于额外的优化而产生的。通过接口(ITest.Method)进行访问会发出callvirt,而通过具体类型(Test.Method)进行访问则会发出call。编译器可能会优化显式接口方法调用。然而,将其留给JIT可能更容易。在这种情况下,Microsoft JIT对隐式/显式调用发出相同的x86代码。 - Will
1个回答

3
看起来接口方法是作为虚拟方法实现的,因此显式实现将覆盖虚拟方法实现。我越想越觉得这很有道理,显式实现实际上是虚拟重载。
我还没有使用mono编译器检查,但这里是在使用csc /target:library /optimize+后从ildasm.exe中获取的转储数据。如您所见,接口方法在接口声明时是虚拟的。当将类型强制转换为接口时,似乎我们正在为该方法提供虚拟重载,而不是在同一类上隐式声明的方法。仍然希望比我更有经验的人能够发表意见。
使用的代码:
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输出:

.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

我现在觉得这对我有意义了。我目前的工作理论是:接口上的方法必须是虚拟的,因为您无法静态确定实现类型是否提供“普通”或虚拟/抽象实现。为确保在通过接口调用时实现类型层次结构上的虚拟方法正常工作,“callvirt”是正确的方法。(还请参阅我对通过接口调用隐式方法的问题的评论)。 - cpt. jazz
没错。我认为JIT可能会足够聪明,一旦它从IL翻译成字节码,就会优化执行,但这是我在知识范围之外做出的假设。 - Jonathon Chase

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接