CIL指令“Call”和“Callvirt”之间有什么区别?
答案 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指令调用对象的后期绑定方法。也就是说,根据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/