C# 为什么Int32.ToString()发出调用指令而不是callvirt?
对于以下代码段:C# 为什么Int32.ToString()发出调用指令而不是callvirt?,c#,.net,clr,cil,boxing,C#,.net,Clr,Cil,Boxing,对于以下代码段: 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();
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
发出constranned
+callvirt
,为Int32
发出call
?这是因为Int
是一个框架提供的密封类型,而且永远不会有其他类型重写IntToString
方法,所以编译器知道它总是需要调用int
类型中提供的ToString()
方法实现,所以它不需要使用callvirt
来确定要调用哪个实现
对于primitve类型,编译器知道调用ToString
的哪个实现,但当我们创建一个自定义值类型时,它是一个以前从未存在过的新类型,所以编译器不知道它,它需要弄清楚调用哪个实现以及它驻留在哪里,由于默认情况下它继承自对象
,因此编译器必须执行callvirt
来定位为自定义类型提供的ToString()
实现,如果不重写,它将调用显而易见的对象类型
以下现有SO帖子可以帮助您理解这一点:
这是编译器对基元类型进行的优化 但是,即使对于自定义结构,
callvirt
实际上也会在运行时作为call
执行,因为受约束。
opcode-在方法被重写的情况下。它允许编译器在两种情况下发出相同的指令,并让运行时处理
发件人:
如果thisType
是一个值类型,并且thisType
实现方法
,则ptr
将作为this
指向调用
方法指令的指针进行传递,而不作任何修改,以便thisType
实现方法
以及:
constrated
操作码允许IL编译器以统一的方式调用虚拟函数,而与ptr
是值类型还是引用类型无关。虽然它适用于thisType
是泛型类型变量的情况,但约束前缀也适用于非泛型类型,并可以降低在隐藏值类型和引用类型之间的区别的语言中生成虚拟调用的复杂性
我不知道有任何关于优化的官方文档,但是你可以在Roslyn回购协议中看到关于优化的评论
至于为什么非原语类型的优化会推迟到运行时,我相信这是因为实现可能会改变。想象一下,引用一个库,该库最初具有对
ToString
的覆盖,然后将DLL(无需重新编译!)更改为删除覆盖的DLL。这会导致运行时异常。对于原语,它们可以确保不会发生这种情况。但是所有的值类型都是隐式密封的,所以我认为从这个角度来看,Int32
和Test
的行为应该是相同的。如果我错了,请纠正我。是的,这是框架提供的密封类型,这是特殊情况抱歉,我不知道你的“特殊情况”在这里是什么意思。你能告诉我更多的细节吗?正如@ali所说,编译器对代码执行的性能进行优化,就像对不能扩展的基本类型进行优化一样“编译器不知道”编译器当然有所有的信息来确定覆盖是否存在,并且可以使用调用
。它只是决定不这样做,原因在@EliArbel的答案末尾解释了。非常感谢,我也猜是编译器优化起作用了。但我找不到任何支持我猜测的材料。因此,如果您能提供关于这个特殊优化的原始类型的文档,我将不胜感激。再次感谢。@LifuHuang更新了我的答案。谢谢@Eli Arbel,你真的解决了我的问题:)