呼叫和 Callvirt

发布于 2024-07-06 17:08:08 字数 37 浏览 6 评论 0原文

CIL指令“Call”和“Callvirt”之间有什么区别?

What is the difference between the CIL instructions "Call" and "Callvirt"?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

花桑 2024-07-13 17:08:08

当运行时执行call指令时,它会调用一段确切的代码(方法)。 毫无疑问它存在于何处。 一旦 IL 被 JITted,调用站点生成的机器代码就是无条件 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 的密封类比非密封类具有更好的方法分派性能。

编辑:事实证明我错了。 C# 编译器无法无条件跳转到方法的位置,因为对象的引用(方法内 this 的值)可能为 null。 相反,它会发出 callvirt 来执行 null 检查并在需要时抛出异常。

这实际上解释了我在使用 Reflector 的 .NET 框架中发现的一些奇怪的代码:

if (this==null) // ...

编译器有可能生成 this 指针 (local0) 具有空值的可验证代码,只有 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

不幸的是,我的电脑目前无法运行,但一旦它再次启动我就会尝试一下。

When the runtime executes a call instruction it's making a call to an exact piece of code (method). There's no question about where it exists. Once the IL has been JITted, the resulting machine code at the call site is an unconditional jmp instruction.

By contrast, the callvirt instruction is used to call virtual methods in a polymorphic way. The exact location of the method's code must be determined at runtime for each and every invocation. The resulting JITted code involves some indirection through vtable structures. Hence the call is slower to execute, but it is more flexible in that it allows for polymorphic calls.

Note that the compiler can emit call instructions for virtual methods. For example:

sealed class SealedObject : object
{
   public override bool Equals(object o)
   {
      // ...
   }
}

Consider calling code:

SealedObject a = // ...
object b = // ...

bool equal = a.Equals(b);

While System.Object.Equals(object) is a virtual method, in this usage there is no way for an overload of the Equals method to exist. SealedObject is a sealed class and cannot have subclasses.

For this reason, .NET's sealed classes can have better method dispatching performance than their non-sealed counterparts.

EDIT: Turns out I was wrong. The C# compiler cannot make an unconditional jump to the method's location because the object's reference (the value of this within the method) might be null. Instead it emits callvirt which does the null check and throws if required.

This actually explains some bizarre code I found in the .NET framework using Reflector:

if (this==null) // ...

It's possible for a compiler to emit verifiable code that has a null value for the this pointer (local0), only csc doesn't do this.

So I guess call is only used for class static methods and structs.

Given this information it now seems to me that sealed is only useful for API security. I found another question that seems to suggest there are no performance benefits to sealing your classes.

EDIT 2: There's more to this than it seems. For example the following code emits a call instruction:

new SealedObject().Equals("Rubber ducky");

Obviously in such a case there is no chance that the object instance could be null.

Interestingly, in a DEBUG build, the following code emits callvirt:

var o = new SealedObject();
o.Equals("Rubber ducky");

This is because you could set a breakpoint on the second line and modify the value of o. In release builds I imagine the call would be a call rather than callvirt.

Unfortunately my PC is currently out of action, but I'll experiment with this once it's up again.

孤星 2024-07-13 17:08:08

call 用于调用非虚拟、静态或超类方法,即调用的目标不受覆盖。 callvirt 用于调用虚拟方法(因此,如果 this 是重写该方法的子类,则调用子类版本)。

call is for calling non-virtual, static, or superclass methods, i.e., the target of the call is not subject to overriding. callvirt is for calling virtual methods (so that if this is a subclass that overrides the method, the subclass version is called instead).

完美的未来在梦里 2024-07-13 17:08:08

因此,.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();
    }
}

注意 类无需密封即可发挥作用。

因此,如果所有这些条件都为真,编译器似乎会发出调用:

  • 方法调用是在对象创建后立即进行的
  • 该方法未在基​​类中实现

For this reason, .NET's sealed classes can have better method dispatching performance than their non-sealed counterparts.

Unfortunately this is not the case. Callvirt does one other thing that makes it useful. When an object has a method called on it callvirt will check if the object exists, and if not throws a NullReferenceException. Call will simply jump to the memory location even if the object reference is not there, and try to execute the bytes in that location.

What this means is that callvirt is always used by the C# compiler (not sure about VB) for classes, and call is always used for structs (because they can never be null or subclassed).

Edit In response to Drew Noakes comment: Yes it seems you can get the compiler to emit a call for any class, but only in the following very specific case:

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();
    }
}

NOTE The class does not have to be sealed for this to work.

So it looks like the compiler will emit a call if all these things are true:

  • The method call is immediately after the object creation
  • The method is not implemented in a base class
时光沙漏 2024-07-13 17:08:08

根据 MSDN:

调用

: call 指令调用随该指令传递的方法描述符指示的方法。 方法描述符是一个元数据标记,指示要调用的方法...元数据标记携带足够的信息来确定调用是静态方法、实例方法、虚拟方法还是全局函数。 在所有这些情况下,目标地址完全由方法描述符确定(与调用虚拟方法的 Callvirt 指令对比,其中目标地址还取决于之前推送的实例引用的运行时类型)卡尔维特)。

CallVirt

callvirt 指令调用对象上的后期绑定方法。 也就是说,方法是根据 obj 的运行时类型而不是方法指针中可见的编译时类来选择的。 Callvirt 可用于调用虚拟方法和实例方法。

所以基本上,采用不同的路线来调用对象的实例方法,无论是否覆盖:

调用:变量 -> 变量的类型对象 -> 方法

CallVirt:变量 -> 对象实例-> 对象的类型对象 -> 方法

According to MSDN:

Call:

The call instruction calls the method indicated by the method descriptor passed with the instruction. The method descriptor is a metadata token that indicates the method to call...The metadata token carries sufficient information to determine whether the call is to a static method, an instance method, a virtual method, or a global function. In all of these cases the destination address is determined entirely from the method descriptor (contrast this with the Callvirt instruction for calling virtual methods, where the destination address also depends upon the runtime type of the instance reference pushed before the Callvirt).

CallVirt:

The callvirt instruction calls a late-bound method on an object. That is, the method is chosen based on the runtime type of obj rather than the compile-time class visible in the method pointer. Callvirt can be used to call both virtual and instance methods.

So basically, different routes are taken to invoke an object's instance method, overriden or not:

Call: variable -> variable's type object -> method

CallVirt: variable -> object instance -> object's type object -> method

踏雪无痕 2024-07-13 17:08:08

也许值得在前面的答案中添加一件事是,
“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; }
    }

首先,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() 移动到某个扩展函数(因为“IL 调用”语义允许它 - 但可惜,失去了对私有成员的访问权限等),您可以自由地将其推入 .GetE()

中,“callvirt[instance]”的执行有更多共同点
使用“call”比使用“callvirt[virtual]”,因为后者可能必须执行三重间接寻址才能找到函数的地址。
(间接到 typedef base,然后到 base-vtab-or-some-interface,然后到实际插槽)

希望这有帮助,
鲍里斯

One thing perhaps worth adding to the previous answers is,
there seems to be only one face to how "IL call" actually executes,
and two faces to how "IL callvirt" executes.

Take this sample setup.

    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; }
    }

First, the CIL body of FInst() and FExt() is 100% identical, opcode-to-opcode
(except that one is declared "instance" and the other "static")
-- however, FInst() will get called with "callvirt" and FExt() with "call".

Second, FInst() and FVirt() will both be called with "callvirt"
-- even though one is virtual but the other isn't --
but it's not the "same callvirt" that will really get to execute.

Here's what roughly happens after 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]  

The only difference between "call" and "callvirt[instance]" is that "callvirt[instance]" intentionally tries to access one byte from *pObj before it calls the direct pointer of the instance function (in order to possibly throw an exception "right there and then").

Thus, if you're annoyed by the number of times that you have to write the "checking part" of

var d = GetDForABC (a, b, c);
var e = d != null ? d.GetE() : ClassD.SOME_DEFAULT_E;

You cannot push "if (this==null) return SOME_DEFAULT_E;" down into ClassD.GetE() itself
(as the "IL callvirt[instance]" semantics prohibits you to do this)
but you're free to push it into .GetE() if you move .GetE() to an extension function somewhere (as the "IL call" semantic allows it -- but alas, losing access to private members etc.)

That said, the execution of "callvirt[instance]" has more in common
with "call" than with "callvirt[virtual]", since the latter may have to execute a triple indirection in order to find the address of your function.
(indirection to typedef base, then to base-vtab-or-some-interface, then to actual slot)

Hope this helps,
Boris

埋葬我深情 2024-07-13 17:08:08

只需添加上述答案,我认为很久以前就已经进行了更改,这样将为所有实例方法生成 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/

Just adding to the above answers, I think the change has been made long back such that Callvirt IL instruction will get generated for all the instance methods and Call IL instruction will get generated for static methods.

Reference :

Pluralsight course "C# Language Internals - Part 1 by Bart De Smet (video -- Call instructions and call stacks in CLR IL in a Nutshell)

and also
https://blogs.msdn.microsoft.com/ericgu/2008/07/02/why-does-c-always-use-callvirt/

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文