为什么Int32.ToString()发出调用指令而不是callvirt
本文关键字:指令 callvirt 调用 ToString Int32 为什么 | 更新日期: 2023-09-27 17:59:23
对于以下代码片段:
struct Test
{
public override string ToString()
{
return "";
}
}
public class Program
{
public static void Main()
{
Test a = new Test();
a.ToString();
Int32 b = 5;
b.ToString();
}
}
编译器发出以下IL:
.locals init ([0] valuetype ConsoleApplication2.Test a,
[1] int32 b)
IL_0000: nop
IL_0001: ldloca.s a
IL_0003: initobj ConsoleApplication2.Test
IL_0009: ldloca.s a
IL_000b: constrained. ConsoleApplication2.Test
IL_0011: callvirt instance string [mscorlib]System.Object::ToString()
IL_0016: pop
IL_0017: ldc.i4.5
IL_0018: stloc.1
IL_0019: ldloca.s b
IL_001b: call instance string [mscorlib]System.Int32::ToString()
IL_0020: pop
IL_0021: ret
由于值类型Test
和Int32
都覆盖了ToString()
方法,我认为a.ToString()
和b.ToString()
中都不会出现装箱。所以我想知道为什么编译器为Test
发出constraned
+callvirt
,为Int32
发出call
?
这是编译器对基元类型进行的优化。
但是,即使对于自定义结构,由于constrained.
操作码,在方法被重写的情况下,callvirt
实际上也会在运行时作为call
执行。它允许编译器在任何一种情况下发出相同的指令,并让运行时处理它
来自MSDN:
如果
thisType
是值类型,并且thisType
实现method
,则ptr
作为指向call
方法指令的this
指针未经修改地传递,以供thisType
实现方法。
和:
constrained
操作码允许IL编译器以统一的方式调用虚拟函数,而与ptr
是值类型还是引用类型无关。尽管它适用于thisType
是泛型类型变量的情况,但受约束前缀也适用于非泛型类型,并且可以降低在隐藏值类型和引用类型之间区别的语言中生成虚拟调用的复杂性。
我不知道任何关于优化的官方文档,但您可以在Roslyn repo中看到MayUseCallForStructMethod
方法的注释。
至于为什么将这种优化推迟到非基元类型的运行时,我认为这是因为实现可能会发生变化。想象一下,引用一个最初具有ToString
覆盖的库,然后将DLL(无需重新编译!)更改为删除覆盖的DLL。这会导致运行时异常。对于基元,他们可以确信这不会发生。
因为Int
是一个框架提供的密封类型,其他类型永远不会覆盖int ToString
方法,所以编译器知道它总是需要调用int
类型中提供的ToString()
方法实现,所以它不需要使用callvirt
来确定要调用哪个实现。
对于原始类型,编译器知道要调用ToString
的哪个实现,但当我们创建自定义值类型时,它是一个以前从未存在过的新类型,所以编译器不知道它,它需要弄清楚要调用哪个实现以及它驻留在哪里,因为它默认继承自Object
,因此编译器必须执行CCD_ 33来定位为自定义类型提供的CCD_。
以下现有的SO帖子可以帮助你理解这一点:
Call和Callvirt