电话和Callvirt

时间:2008-10-11 10:26:59

标签: .net reflection cil reflection.emit

CIL指令“Call”和“Callvirt”之间有什么区别?

6 个答案:

答案 0 :(得分:53)

当运行时执行call指令时,它正在调用一段确切的代码(方法)。毫无疑问它存在于何处。 一旦IL被JIT,在调用站点生成的机器代码是无条件的jmp指令。

相比之下,callvirt指令用于以多态方式调用虚方法。必须在运行时为每次调用确定方法代码的确切位置。生成的JITted代码涉及通过vtable结构的一些间接。因此,调用执行起来较慢,但它更灵活,因为它允许多态调用。

请注意,编译器可以为虚拟方法发出call条指令。例如:

sealed class SealedObject : object
{
   public override bool Equals(object o)
   {
      // ...
   }
}

考虑调用代码:

SealedObject a = // ...
object b = // ...

bool equal = a.Equals(b);

虽然System.Object.Equals(object)是一种虚方法,但在此用法中,无法存在Equals方法的重载。 SealedObject是一个密封的类,不能有子类。

出于这个原因,.NET的sealed类可以比非密封的类具有更好的方法调度性能。

编辑:原来我错了。 C#编译器无法无条件跳转到方法的位置,因为对象的引用(方法中的this的值)可能为null。相反,它会发出callvirt进行空检查并在必要时抛出。

这实际上解释了我在.NET框架中使用Reflector找到的一些奇怪的代码:

if (this==null) // ...

编译器可以发出可验证的代码,该代码具有this指针(local0)的空值,只有csc不会这样做。

所以我猜call仅用于类静态方法和结构。

鉴于此信息,我现在认为sealed仅对API安全性有用。我发现another question似乎表明密封课程没有性能优势。

编辑2:除此之外还有更多内容。例如,以下代码发出call指令:

new SealedObject().Equals("Rubber ducky");

显然在这种情况下,对象实例不可能为null。

有趣的是,在DEBUG构建中,以下代码会发出callvirt

var o = new SealedObject();
o.Equals("Rubber ducky");

这是因为您可以在第二行设置断点并修改o的值。在发布版本中,我认为调用将是call而不是callvirt

不幸的是我的电脑目前还没有动作,但是一旦重新启动我就会试验一下。

答案 1 :(得分:48)

call用于调用非虚拟,静态或超类方法,即调用的目标不受覆盖。 callvirt用于调用虚方法(如果this是覆盖该方法的子类,则调用子类版本。)

答案 2 :(得分:11)

  

出于这个原因,.NET的密封类可以比非密封类具有更好的方法调度性能。

不幸的是情况并非如此。 Callvirt做了另一件让它变得有用的事情。当一个对象有一个调用它的方法时,callvirt将检查该对象是否存在,如果没有抛出NullReferenceException。即使对象引用不存在,调用也只会跳转到内存位置,并尝试执行该位置的字节。

这意味着callvirt总是由C#编译器(不确定VB)用于类,并且call总是用于结构(因为它们永远不能为null或子类)。

编辑响应Drew Noakes评论:是的,似乎你可以让编译器为任何类发出一个调用,但仅限于以下非常具体的情况:

public class SampleClass
{
    public override bool Equals(object obj)
    {
        if (obj.ToString().Equals("Rubber Ducky", StringComparison.InvariantCultureIgnoreCase))
            return true;

        return base.Equals(obj);
    }

    public void SomeOtherMethod()
    {
    }

    static void Main(string[] args)
    {
        // This will emit a callvirt to System.Object.Equals
        bool test1 = new SampleClass().Equals("Rubber Ducky");

        // This will emit a call to SampleClass.SomeOtherMethod
        new SampleClass().SomeOtherMethod();

        // This will emit a callvirt to System.Object.Equals
        SampleClass temp = new SampleClass();
        bool test2 = temp.Equals("Rubber Ducky");

        // This will emit a callvirt to SampleClass.SomeOtherMethod
        temp.SomeOtherMethod();
    }
}

注意为了实现此目的,不必密封课程。

如果所有这些都是真的,那么看起来编译器会发出一个调用:

  • 方法调用在对象创建后立即进行
  • 该方法未在基​​类中实现

答案 3 :(得分:6)

根据MSDN:

Call

调用指令调用由指令传递的方法描述符指示的方法。方法描述符是指示要调用的方法的元数据标记...元数据标记携带足够的信息以确定调用是静态方法,实例方法,虚拟方法还是全局函数。 在所有这些情况下,目标地址完全由方法描述符确定(与调用虚拟方法的Callvirt指令形成对比,其中目标地址还取决于之前推送的实例引用的运行时类型Callvirt)。

CallVirt

callvirt指令调用对象的后期绑定方法。也就是说,根据obj的运行时类型而不是方法指针中可见的编译时类来选择方法。 Callvirt可用于调用虚拟和实例方法。

基本上,采用不同的路由来调用对象的实例方法,覆盖与否:

致电:变量 - > 变量的类型对象 - >方法

CallVirt:变量 - >对象实例 - > 对象的类型对象 - >方法

答案 4 :(得分:2)

或许值得添加到之前答案的一件事是, 似乎只有一个面孔如何" IL呼叫"实际执行, 和两个面孔如何" IL callvirt"执行。

进行此样本设置。

    public class Test {
        public int Val;
        public Test(int val)
            { Val = val; }
        public string FInst () // note: this==null throws before this point
            { return this == null ? "NO VALUE" : "ACTUAL VALUE " + Val; }
        public virtual string FVirt ()
            { return "ALWAYS AN ACTUAL VALUE " + Val; }
    }
    public static class TestExt {
        public static string FExt (this Test pObj) // note: pObj==null passes
            { return pObj == null ? "NO VALUE" : "VALUE " + pObj.Val; }
    }

首先,FInst()和FExt()的CIL主体是100%相同的,操作码到操作码 (除了声明一个"实例"另一个"静态") - 但是,FInst()将被调用" callvirt"和FExt()用"调用"。

其次,FInst()和FVirt()都会被调用" callvirt" - 即使一个是虚拟的但另一个不是 - 但它不是"同样的callvirt"这将真正得到执行。

这是JITting之后大致发生的事情:

    pObj.FExt(); // IL:call
    mov         rcx, <pObj>
    call        (direct-ptr-to) <TestExt.FExt>

    pObj.FInst(); // IL:callvirt[instance]
    mov         rax, <pObj>
    cmp         byte ptr [rax],0
    mov         rcx, <pObj>
    call        (direct-ptr-to) <Test.FInst>

    pObj.FVirt(); // IL:callvirt[virtual]
    mov         rax, <pObj>
    mov         rax, qword ptr [rax]  
    mov         rax, qword ptr [rax + NNN]  
    mov         rcx, <pObj>
    call        qword ptr [rax + MMM]  

&#34; call&#34;之间的唯一区别和&#34; callvirt [实例]&#34;就是&#34; callvirt [实例]&#34;故意在调用实例函数的直接指针之前尝试从* pObj访问一个字节(为了可能抛出异常&#34;就在那里然后&#34;)。

因此,如果您对您必须编写&#34;检查部分&#34;的次数感到烦恼?的

var d = GetDForABC (a, b, c);
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E;

你无法推送&#34; if(this == null)返回SOME_DEFAULT_E;&#34;进入ClassD.GetE()本身 (因为&#34; IL callvirt [instance]&#34;语义禁止你这样做) 但你可以自由地将它推入.GetE(),如果你将.GetE()移动到某个地方的扩展函数(因为&#34; IL调用&#34;语义允许它 - 但是唉,失去访问权限私人会员等。)

那就是说,执行&#34; callvirt [instance]&#34;有更多的共同点 与&#34;电话&#34;而使用&#34; callvirt [virtual]&#34;,因为后者可能必须执行三重间接才能找到你的函数的地址。 (间接到typedef base,然后到base-vtab-or-some-interface,然后到实际插槽)

希望这有帮助, 鲍里斯

答案 5 :(得分:1)

只是添加上述答案,我认为已经做了很长时间的更改,以便为所有实例方法生成Callvirt IL指令,并为静态方法生成调用IL指令。

参考:

Pluralsight课程&#34; C#语言内部 - 第1部分作者:Bart De Smet(视频 - CLR IL中的调用指令和调用栈)#/ p>

还有 https://blogs.msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/