当运行时执行 call
指令时,它是在调用一个确切的代码片段(方法)。没有关于它存在的问题。一旦中间语言(IL)被即时编译(JIT),调用站点上的结果机器码是无条件的 jmp
指令。
相比之下,callvirt
指令用于以多态方式调用虚方法。必须为每次调用确定方法代码的确切位置。JIT后生成的代码涉及通过虚表结构的某些间接性。因此,调用执行较慢,但更具灵活性,因为它允许进行多态调用。
请注意,编译器可以为虚方法发出 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
值)可能为空。因此,它发出 callvirt
指令来进行空检查,并在必要时抛出异常。
这实际上解释了我在 .NET 框架中使用反编译器找到的一些奇怪代码:
if (this==null) // ...
编译器可以生成可验证的代码,并将 this
指针(local0)设置为 null 值,但 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
是子类且覆盖了该方法,则调用子类版本)。
call
在执行调用之前不会检查指针是否为 null,而 callvirt
显然需要这样做。这就是为什么编译器有时会生成 callvirt
,即使调用的是非虚方法。 - dallecallvirt
,这很公平。无论如何,我对call
和callvirt
的描述(据我所知)仍然是正确的,而且我的评论关于JVM字节码的工作方式仍然成立——使用super.foobar()
的Java代码确实仍然会使用invokespecial
(而不是invokevirtual
)。 - C. K. YoungIL_0026: ldstr "This was expected behavior. Will exit with code 100."
IL_002b: *call* void [mscorlib]System.Console::WriteLine(string)
IL_0030: ldc.i4.s 100```
- Jduvpublic 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();
}
}
注意,这个功能不需要将类密封。
因此,如果以下所有条件均为真,则编译器似乎会发出调用:
call
。 - Antony Thomas调用:变量 -> 变量的 类型对象 -> 方法
虚方法调用:变量 -> 对象实例 -> 对象的 类型对象 -> 方法
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主体是完全相同的,从操作码到操作码都是一样的 (除了一个被声明为“实例”,另一个被声明为“静态”) - 但是FInst()将使用“callvirt”调用,而FExt()将使用“call”调用。
其次,尽管其中一个是虚拟的,而另一个不是, 但FInst()和FVirt()都将使用“callvirt”进行调用 - 但实际执行的“callvirt”并不是完全相同的。
以下是JIT编译后大致发生的情况:
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;
除了以上回答,我认为早在很久以前就已经进行了更改,使得对于所有实例方法都会生成Callvirt IL指令,而对于静态方法则会生成Call IL指令。
参考资料:
Pluralsight课程“C#语言内部-第1部分”(由Bart De Smet制作的视频-- CLR IL中的调用指令和调用堆栈概述)
以及https://blogs.msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/
call
也被用于基础调用。此外,我相信,如果Roslyn编译器可以轻松确定对象永远不会为空(例如,从空条件运算符生成的调用),则它将为密封/非虚拟方法生成 call
指令。 - Brian Reichle