电话和Callvirt
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了电话和Callvirt相关的知识,希望对你有一定的参考价值。
CIL指令“Call”和“Callvirt”之间有什么区别?
call
用于调用非虚拟,静态或超类方法,即调用的目标不受覆盖。 callvirt
用于调用虚方法(因此,如果this
是覆盖该方法的子类,则调用子类版本)。
当运行时执行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安全性有用。我发现another question似乎表明密封你的课程没有性能上的好处。
编辑2:除此之外还有更多内容。例如,以下代码发出call
指令:
new SealedObject().Equals("Rubber ducky");
显然,在这种情况下,对象实例不可能为null。
有趣的是,在DEBUG构建中,以下代码发出callvirt
:
var o = new SealedObject();
o.Equals("Rubber ducky");
这是因为您可以在第二行设置断点并修改o
的值。在发布版本中,我想这个调用将是call
而不是callvirt
。
不幸的是我的PC目前还没有动作,但是一旦它重新上升,我会试验一下。
出于这个原因,.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:
Call:
调用指令调用由指令传递的方法描述符指示的方法。方法描述符是指示要调用的方法的元数据标记...元数据标记携带足够的信息以确定调用是静态方法,实例方法,虚拟方法还是全局函数。在所有这些情况下,目标地址完全由方法描述符确定(与调用虚拟方法的Callvirt指令形成对比,其中目标地址还取决于在Callvirt之前推送的实例引用的运行时类型)。
callvirt指令调用对象的后期绑定方法。也就是说,该方法是基于obj的运行时类型而不是方法指针中可见的编译时类来选择的。 Callvirt可用于调用虚拟和实例方法。
所以基本上,采用不同的路由来调用对象的实例方法,覆盖或不覆盖:
调用:变量 - >变量的类型对象 - >方法
CallVirt:变量 - >对象实例 - >对象的类型对象 - >方法
也许值得添加到之前的答案的一件事是,似乎只有一个面对“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()都将使用“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]
“call”和“callvirt [instance]”之间的唯一区别是“callvirt [instance]”故意在调用实例函数的直接指针之前尝试从* pObj访问一个字节(为了可能引发异常“就在那里,然后“)。
因此,如果您对您必须编写“检查部分”的次数感到恼火
var d = GetDForABC (a, b, c);
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E;
你不能推“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,然后到实际的slot)
希望这有帮助,鲍里斯
只是添加到上面的答案,我认为已经做了很长的改变,以便为所有实例方法生成Callvirt IL指令,并且将为静态方法生成调用IL指令。
参考:
Pluralsight课程“C#语言内部 - 第1部分:Bart De Smet(视频 - CLR IL中的调用指令和调用栈)
还有https://blogs.msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/
以上是关于电话和Callvirt的主要内容,如果未能解决你的问题,请参考以下文章