CALL AND CALLVIRT IN CIL

来源:互联网 发布:js window.open 编辑:程序博客网 时间:2024/06/16 17:04

如果你对于.NET 中间语言有一定了解,你一定注意到 CIL 提供了2个方法调用指令:call 和 callvirt。本博文将简要介绍着2个指令并让你对它们的使用有一个大致的了解。


call - The basics

call 指令在 CIL 中提供基本的指令调用功能,让我们看看下面的例子:

class Program{        static void Main(string[] args)        {                Printer.Print("Hello World");        }}public class Printer{        public static void Print(string message)        {                Console.WriteLine(message);        }}

.method private hidebysig static void  Main(string[] args) cil managed{  .entrypoint  // 代码大小       13 (0xd)  .maxstack  8  IL_0000:  nop  IL_0001:  ldstr      "Hello World"  IL_0006:  call       void CSharp.CLR.Program/Printer::Print(string)  IL_000b:  nop  IL_000c:  ret} // end of method Program::Main

.method public hidebysig static void  Print(string message) cil managed{  // 代码大小       9 (0x9)  .maxstack  8  IL_0000:  nop  IL_0001:  ldarg.0  IL_0002:  call       void [mscorlib]System.Console::WriteLine(string)  IL_0007:  nop  IL_0008:  ret} // end of method Printer::Print

这个例子并没有什么实用价值,只是作为示范作用。当我们执行 Main 方法时,“Hello World” 被加载到堆栈中,然后 Print 方法(只有一个string参数)被调用(使用call指令)。注意 call 指令本身使用了一个引用描述符(这个引用实际上是一个元数据标记,关于元数据的细节不在本文讨论范围内)。当 call 指令执行时,它将方法执行所需要的参数出栈,并将该参数作为参数数组中索引为0的项传递给方法(可参考Print 方法中的 IL_0000 行)。在 Print 方法的 IL 代码中,我们可以看到取出的参数做为 “argument.0” 被传递给方法 Console.WriteLine,而这是通过另一个 call 调用实现的。在本例中,Print 方法不返回任何值,但是如果它返回了一个值,那么这个值会在 “ret” 指令调用前被压入栈中。


callvirt - The basics

也许区分 call 和 callvirt 指令最简单的方法就是查看 CIL 文档,其中 call 只是简单得用来"调用由传递的方法说明符指示的方法",而 callvirt 是用来 “对对象调用后期绑定方法,并且将返回值推送到计算堆栈上”。为了理解这两段描述的不同,我们来看一个例子:

public static void Print(object thingy){        Console.WriteLine(thingy.ToString());}

你也许已经知道,这个方法的行为取决于 “thingy” 这个对象的实际类型,因为 ToString 实际上是一个虚方法。但是运行时如何知道哪一个 ToString 方法的实现会在这个例子中被调用呢?这正是引入 callvirt 指令的意义所在。当使用 callvirt 指令调用虚方法时,CLR 会调查发出调用的那个对象的实际类型,然后以多态的方式调用方法。而为了执行一个 callvirt 指令,唯一需要做的事情就是在调用相应的方法时将一个指向对象引用的指针传递给 callvirt,可以参考下面的 IL 代码:

.method public hidebysig static void  Print(object thingy) cil managed{  // 代码大小       14 (0xe)  .maxstack  8  IL_0000:  nop  IL_0001:  ldarg.0  IL_0002:  callvirt   instance string [mscorlib]System.Object::ToString()  IL_0007:  call       void [mscorlib]System.Console::WriteLine(string)  IL_000c:  nop  IL_000d:  ret} // end of method Printer::Print

我们重写的 ToString 方法没有任何参数,然后在调用前,索引0处的参数值仍然会出栈,这个参数值就是 “thingy” 本身。当执行 callvirt 指令调用 ToString 方法时,它首先检查 “thingy” 是否为 null,然后它将沿着继承树查找一个正确实现了 ToSting 方法的实例,最终定位到类型 “thingy” 上。


When callvirt replaces call ...

到目前为止,我们讨论了 call 指令和 callvirt 指令的不同,call 指令提供了简单的方法调用功能,而 callvirt 指令则提供多态得调用虚方法的能力。然而,如果你开始检查你自己的 C# 程序,你将会发现 callvirt 指令也被用来调用非虚实例方法。那么 C# 编译器为什么要这么做?以下是两个理由:

  1. Nonvirtual methods can be made virtual without recompiling calling assemblies. (这句话没太读懂)
  2. C# 工作组认为,JIT 编译器应生成代码来核实发出调用的对象不为 NULL
这里你需要理解的一点是:使用 callvirt 指令调用非虚方法并不会对性能造成太大的影响,虽然使用 callvirt 调用非虚方法也会执行 null check。当 JIT 编辑器了解到调用的方法是一个非虚方法时,它不会沿着继承树查找这个方法的正确实现,而是像 call 一样直接调用该方法。这让 callvirt 在调用非虚方法时几乎和 call 一样快。

When call replaces callvirt ...

尽管名字中没有 “virt” 字样,call 指令有时仍然会被用来调用虚方法。一个例子就是,当一个重写的方法中显示调用了基类实现,如:
public override string ToString(){        base.ToString();}

如果使用 callvirt 指令调用 base.ToString() 方法,那么调用会递归执行,直至线程栈溢出。

原文地址:点击打开链接

0 0