.net 呼叫和呼叫
CIL指令“Call”和“Callvirt”之间的区别是什么?.net 呼叫和呼叫,.net,reflection,cil,reflection.emit,.net,Reflection,Cil,Reflection.emit,CIL指令“Call”和“Callvirt”之间的区别是什么?Call用于调用非虚拟、静态或超类方法,即调用的目标不受覆盖callvirt用于调用虚拟方法(因此,如果this是重写该方法的子类,则将调用子类版本)。当运行时执行call指令时,它正在调用一段精确的代码(方法)。毫无疑问,它存在于何处。一旦IL被JIT,在调用站点生成的机器代码就是一条无条件的jmp指令 相反,callvirt指令用于以多态方式调用虚拟方法。每次调用都必须在运行时确定方法代码的确切位置。由此产生的JITted代码涉及
Call
用于调用非虚拟、静态或超类方法,即调用的目标不受覆盖callvirt
用于调用虚拟方法(因此,如果this
是重写该方法的子类,则将调用子类版本)。当运行时执行call
指令时,它正在调用一段精确的代码(方法)。毫无疑问,它存在于何处。一旦IL被JIT,在调用站点生成的机器代码就是一条无条件的jmp
指令
相反,callvirt
指令用于以多态方式调用虚拟方法。每次调用都必须在运行时确定方法代码的确切位置。由此产生的JITted代码涉及通过vtable结构的一些间接寻址。因此,调用的执行速度较慢,但它更灵活,因为它允许多态调用
请注意,编译器可以为虚拟方法发出调用
指令。例如:
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的密封的
类可以比非密封的类具有更好的方法调度性能
编辑:结果证明我错了。C#编译器无法无条件跳转到方法的位置,因为对象的引用(方法中的this
的值)可能为空。相反,它会发出callvirt
,执行空检查并在需要时抛出
这实际上解释了我在使用Reflector的.NET framework中发现的一些奇怪代码:
if (this==null) // ...
编译器可能会发出可验证的代码,该代码的this
指针(local0)值为空,只有csc不会这样做
所以我猜call
只用于类静态方法和结构
考虑到这些信息,我现在觉得密封的只对API安全有用。我发现这似乎表明封闭类并没有性能上的好处
编辑2:这比看起来要复杂得多。例如,以下代码发出调用
指令:
new SealedObject().Equals("Rubber ducky");
显然,在这种情况下,对象实例不可能为null
有趣的是,在调试构建中,以下代码会发出callvirt
:
var o = new SealedObject();
o.Equals("Rubber ducky");
这是因为您可以在第二行设置断点并修改o
的值。在发布版本中,我认为调用应该是call
,而不是callvirt
不幸的是,我的电脑目前无法运行,但我会在它再次启动后进行试验
因此,.NET的密封类可以比非密封类具有更好的方法调度性能
不幸的是,情况并非如此。Callvirt做了另一件事,使它变得有用。当一个对象上调用了一个方法时,callvirt将检查该对象是否存在,如果不存在,则抛出NullReferenceException。调用将直接跳转到内存位置,即使对象引用不存在,并尝试在该位置执行字节
这意味着C#编译器(不确定VB)总是对类使用callvirt,而对结构使用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:variable->object instance->对象的type object->method在前面的答案中,可能值得补充的一点是,
“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()都将用“cal”调用
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]
var d = GetDForABC (a, b, c);
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E;