.net 呼叫和呼叫

.net 呼叫和呼叫,.net,reflection,cil,reflection.emit,.net,Reflection,Cil,Reflection.emit,CIL指令“Call”和“Callvirt”之间的区别是什么?Call用于调用非虚拟、静态或超类方法,即调用的目标不受覆盖callvirt用于调用虚拟方法(因此,如果this是重写该方法的子类,则将调用子类版本)。当运行时执行call指令时,它正在调用一段精确的代码(方法)。毫无疑问,它存在于何处。一旦IL被JIT,在调用站点生成的机器代码就是一条无条件的jmp指令 相反,callvirt指令用于以多态方式调用虚拟方法。每次调用都必须在运行时确定方法代码的确切位置。由此产生的JITted代码涉及

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

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

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

相反,
callvirt
指令用于以多态方式调用虚拟方法。每次调用都必须在运行时确定方法代码的确切位置。由此产生的JITted代码涉及通过vtable结构的一些间接寻址。因此,调用的执行速度较慢,但它更灵活,因为它允许多态调用

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

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的
密封的
类可以比非密封的类具有更好的方法调度性能

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

这实际上解释了我在使用Reflector的.NET framework中发现的一些奇怪代码:

if (this==null) // ...
编译器可能会发出可验证的代码,该代码的
this
指针(local0)值为空,只有csc不会这样做

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

考虑到这些信息,我现在觉得
密封的
只对API安全有用。我发现这似乎表明封闭类并没有性能上的好处

编辑2:这比看起来要复杂得多。例如,以下代码发出
调用
指令:

new SealedObject().Equals("Rubber ducky");
显然,在这种情况下,对象实例不可能为null

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

var o = new SealedObject();
o.Equals("Rubber ducky");
这是因为您可以在第二行设置断点并修改
o
的值。在发布版本中,我认为调用应该是
call
,而不是
callvirt

不幸的是,我的电脑目前无法运行,但我会在它再次启动后进行试验

因此,.NET的密封类可以比非密封类具有更好的方法调度性能

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

这意味着C#编译器(不确定VB)总是对类使用callvirt,而对结构使用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();
    }
}
注意该类无需密封即可运行

因此,如果所有这些都是真的,编译器将发出一个调用:

  • 方法调用在对象创建之后立即进行
  • 该方法未在基类中实现
根据MSDN:

:

call指令调用随指令传递的方法描述符所指示的方法。方法描述符是一个元数据令牌,指示要调用的方法……元数据令牌包含足够的信息,以确定调用是静态方法、实例方法、虚拟方法还是全局函数在所有这些情况下,目标地址完全由方法描述符确定(与调用虚拟方法的Callvirt指令相反,其中目标地址还取决于在Callvirt之前推送的实例引用的运行时类型)

:

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

因此,基本上,调用对象的实例方法会采用不同的路径,不管是否覆盖:

调用:变量->变量的类型对象->方法


CallVirt:variable->object instance->对象的type object->method

在前面的答案中,可能值得补充的一点是, “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()将被“call”调用

其次,FInst()和FVirt()都将用“cal”调用
    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]  
var d = GetDForABC (a, b, c);
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E;