电话和Callvirt

Eri*_*ith 57 .net reflection cil reflection.emit

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

Dre*_*kes 54

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

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

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

sealed class SealedObject : object
{
   public override bool Equals(object o)
   {
      // ...
   }
}
Run Code Online (Sandbox Code Playgroud)

考虑调用代码:

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

bool equal = a.Equals(b);
Run Code Online (Sandbox Code Playgroud)

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

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

编辑:原来我错了.C#编译器无法无条件跳转到方法的位置,因为对象的引用(方法中的值this)可能为null.相反,callvirt如果需要,它会发出执行null检查和抛出的内容.

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

if (this==null) // ...
Run Code Online (Sandbox Code Playgroud)

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

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

鉴于此信息,我现在认为sealed它仅对API安全性有用.我发现另一个问题似乎表明密封你的课程没有性能上的好处.

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

new SealedObject().Equals("Rubber ducky");
Run Code Online (Sandbox Code Playgroud)

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

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

var o = new SealedObject();
o.Equals("Rubber ducky");
Run Code Online (Sandbox Code Playgroud)

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

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

  • 通过反射寻找它们时,密封属性肯定更快,但除此之外,我不知道你没有提到的任何其他好处. (2认同)

Chr*_*ung 49

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

  • 如果我没记错,`call`在执行调用之前没有检查指针是否为null,那么`callvirt`显然需要.这就是为什么即使调用非虚方法,编译器有时会发出`callvirt`. (37认同)
  • ```call```指令*实际上是*实际发出的.从测试C#程序的ILDasm:```IL_0021:callvirt实例void [ClassLibrary1] ClassLibrary1.Class1 :: sayHello()IL_0026:ldstr"这是预期的行为.将以代码100退出." IL_002b:*call*void [mscorlib] System.Console :: WriteLine(string)IL_0030:ldc.i4.s 100``` (3认同)
  • 啊。感谢您指出这一点(我不是.NET人员)。我使用的类比是JVM字节码中的call => invokespecial和callvirt => invokevirtual。对于JVM,两条指令都检查“ this”是否为空(我只是编写了一个测试程序来检查)。 (2认同)
  • 您可能想在答案中提及性能差异,这就是完全接受"呼叫"指令的原因. (2认同)

Cam*_*and 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

注意为了使其工作,不必密封该类.

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

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


smw*_*dia 6

根据MSDN:

致电:

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

CallVirt:

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

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

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

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


小智 6

也许值得在前面的答案中添加的一件事是,“IL call”实际执行方式似乎只有一个方面,而“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; }
    }
Run Code Online (Sandbox Code Playgroud)

首先,FInst() 和 FExt() 的 CIL 主体在操作码到操作码方面是 100% 相同的(除了一个被声明为“实例”而另一个被声明为“静态”)——但是,FInst() 将被调用“callvirt”和 FExt() 与“call”。

其次,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]  
Run Code Online (Sandbox Code Playgroud)

“call”和“callvirt[instance]”之间的唯一区别是“callvirt[instance]”在调用实例函数的直接指针之前有意尝试从 *pObj 访问一个字节(以便可能抛出异常”就在那里,然后”)。

因此,如果您对必须编写的“检查部分”的次数感到恼火

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

您不能推送“if (this==null) return SOME_DEFAULT_E;” 向下进入 ClassD.GetE() 本身(因为“IL callvirt[instance]”语义禁止您这样做),但如果您将 .GetE() 移动到某个扩展函数,您可以自由地将其推入 .GetE() 中(正如“IL 调用”语义所允许的那样——但可惜的是,失去了对私有成员的访问权限等。)

也就是说,“callvirt[instance]”的执行与“call”比“callvirt[virtual]”有更多共同点,因为后者可能必须执行三重间接寻址才能找到函数的地址。(间接到 typedef base,然后到 base-vtab-or-some-interface,然后到实际插槽)

希望这有帮助,鲍里斯