CIL指令"Call"和"Callvirt"之间有什么区别?
当运行时执行一条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
.
不幸的是,我的电脑目前还没有动作,但是一旦它重新启动我就会试验一下.
call
用于调用非虚拟,静态或超类方法,即调用的目标不受覆盖.callvirt
用于调用虚方法(因此,如果this
是覆盖该方法的子类,则调用子类版本).
出于这个原因,.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(); } }
注意为了使其工作,不必密封该类.
所以如果所有这些都是真的,看起来编译器会发出一个调用:
方法调用在对象创建之后立即进行
该方法未在基类中实现
根据MSDN:
致电:
调用指令调用由指令传递的方法描述符指示的方法.方法描述符是指示要调用的方法的元数据标记...元数据标记携带足够的信息以确定调用是静态方法,实例方法,虚拟方法还是全局函数.在所有这些情况下,目标地址完全由方法描述符确定(与调用虚拟方法的Callvirt指令形成对比,其中目标地址还取决于在Callvirt之前推送的实例引用的运行时类型).
CallVirt:
callvirt指令调用对象的后期绑定方法.也就是说,该方法是基于obj的运行时类型而不是方法指针中可见的编译时类来选择的.Callvirt可用于调用虚拟和实例方法.
所以基本上,采用不同的路由来调用对象的实例方法,覆盖或不覆盖:
调用:变量 - > 变量的类型对象 - >方法
CallVirt:变量 - >对象实例 - > 对象的类型对象 - >方法