当前位置:  开发笔记 > 编程语言 > 正文

电话和Callvirt

如何解决《电话和Callvirt》经验,为你挑选了4个好方法。

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



1> Drew Noakes..:

当运行时执行一条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如果需要,它会发出执行null检查和抛出的内容.

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

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

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

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

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

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

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

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

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

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

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

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


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

2> Chris Jester..:

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


如果我没记错,`call`在执行调用之前没有检查指针是否为null,那么`callvirt`显然需要.这就是为什么即使调用非虚方法,编译器有时会发出`callvirt`.
```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```
啊。感谢您指出这一点(我不是.NET人员)。我使用的类比是JVM字节码中的call => invokespecial和callvirt => invokevirtual。对于JVM,两条指令都检查“ this”是否为空(我只是编写了一个测试程序来检查)。
您可能想在答案中提及性能差异,这就是完全接受"呼叫"指令的原因.

3> Cameron MacF..:

出于这个原因,.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();
    }
}

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

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

方法调用在对象创建之后立即进行

该方法未在基​​类中实现



4> smwikipedia..:

根据MSDN:

致电:

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

CallVirt:

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

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

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

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

推荐阅读
个性2402852463
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有