Dre*_*kes 54
当运行时执行一条call
指令时,它正在调用一段确切的代码(方法).毫无疑问它存在于何处. 一旦IL被JIT,呼叫站点的结果机器代码就是无条件jmp
指令.
相反,该callvirt
指令用于以多态方式调用虚方法.必须在运行时为每次调用确定方法代码的确切位置.生成的JITted代码涉及通过vtable结构的一些间接.因此,调用执行起来较慢,但它更灵活,因为它允许多态调用.
请注意,编译器可以发出call
虚拟方法的指令.例如:
sealed class SealedObject : object
{
public override bool Equals(object o)
{
// ...
}
}
Run Code Online (Sandbox Code Playgroud)
考虑调用代码:
SealedObject a = // ...
object b = // ...
bool equal = a.Equals(b);
Run Code Online (Sandbox Code Playgroud)
虽然System.Object.Equals(object)
是一种虚方法,但在此用法中,无法存在方法的重载Equals
. SealedObject
是一个密封的类,不能有子类.
因此,.NET的sealed
类可以比非密封的类具有更好的方法调度性能.
编辑:原来我错了.C#编译器无法无条件跳转到方法的位置,因为对象的引用(方法中的值this
)可能为null.相反,callvirt
如果需要,它会发出执行null检查和抛出的内容.
这实际上解释了我在.NET框架中使用Reflector找到的一些奇怪的代码:
if (this==null) // ...
Run Code Online (Sandbox Code Playgroud)
编译器可以发出具有this
指针空值的可验证代码(local0),只有csc不会这样做.
所以我猜call
这只是用于类静态方法和结构.
鉴于此信息,我现在认为sealed
它仅对API安全性有用.我发现另一个问题似乎表明密封你的课程没有性能上的好处.
编辑2:除此之外还有更多内容.例如,以下代码发出call
指令:
new SealedObject().Equals("Rubber ducky");
Run Code Online (Sandbox Code Playgroud)
显然,在这种情况下,对象实例不可能为null.
有趣的是,在DEBUG构建中,以下代码会发出callvirt
:
var o = new SealedObject();
o.Equals("Rubber ducky");
Run Code Online (Sandbox Code Playgroud)
这是因为您可以在第二行设置断点并修改其值o
.在发布版本中,我想这个调用将是一个call
而不是callvirt
.
不幸的是,我的电脑目前还没有动作,但是一旦它重新启动我就会试验一下.
Chr*_*ung 49
call
用于调用非虚拟,静态或超类方法,即调用的目标不受覆盖.callvirt
用于调用虚方法(因此,如果this
是覆盖该方法的子类,则调用子类版本).
Cam*_*and 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();
}
}
Run Code Online (Sandbox Code Playgroud)
注意为了使其工作,不必密封该类.
所以如果所有这些都是真的,看起来编译器会发出一个调用:
根据MSDN:
致电:
调用指令调用由指令传递的方法描述符指示的方法.方法描述符是指示要调用的方法的元数据标记...元数据标记携带足够的信息以确定调用是静态方法,实例方法,虚拟方法还是全局函数.在所有这些情况下,目标地址完全由方法描述符确定(与调用虚拟方法的Callvirt指令形成对比,其中目标地址还取决于在Callvirt之前推送的实例引用的运行时类型).
callvirt指令调用对象的后期绑定方法.也就是说,该方法是基于obj的运行时类型而不是方法指针中可见的编译时类来选择的.Callvirt可用于调用虚拟和实例方法.
所以基本上,采用不同的路由来调用对象的实例方法,覆盖或不覆盖:
调用:变量 - > 变量的类型对象 - >方法
CallVirt:变量 - >对象实例 - > 对象的类型对象 - >方法
小智 6
也许值得在前面的答案中添加的一件事是,“IL call”实际执行方式似乎只有一个方面,而“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; }
}
Run Code Online (Sandbox Code Playgroud)
首先,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]
Run Code Online (Sandbox Code Playgroud)
“call”和“callvirt[instance]”之间的唯一区别是“callvirt[instance]”在调用实例函数的直接指针之前有意尝试从 *pObj 访问一个字节(以便可能抛出异常”就在那里,然后”)。
因此,如果您对必须编写的“检查部分”的次数感到恼火
var d = GetDForABC (a, b, c);
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E;
Run Code Online (Sandbox Code Playgroud)
您不能推送“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,然后到实际插槽)
希望这有帮助,鲍里斯
归档时间: |
|
查看次数: |
14223 次 |
最近记录: |