为什么 Int32.ToString() 发出调用指令而不是 callvirt?
Why Int32.ToString() emit call instruction instead of callvirt?
对于以下代码片段:
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
?
因为Int
是框架提供的密封类型,不会出现其他类型覆盖intToString
方法的情况,所以编译器知道它总是需要调用ToString()
方法实现在 int
类型中提供,因此不需要使用 callvirt
来确定调用哪个实现。
对于原始类型,编译器知道要调用 ToString
的哪个实现,但是当我们创建一个自定义值类型时,它是一个新的,它以前不存在,所以编译器不知道它它需要弄清楚要调用哪个实现以及它所在的位置,因为它默认继承自 Object
,因此编译器必须执行 callvirt
来定位提供的 ToString()
实现对于自定义类型,如果未覆盖,它将调用显而易见的对象类型。
以下现有的 SO 帖子可以帮助您理解这一点:
Call and Callvirt
这是编译器针对基本类型所做的优化。
但即使对于自定义结构,由于 constrained.
操作码,callvirt
实际上会在运行时作为 call
执行 - 在方法被覆盖的情况下。它允许编译器在任何一种情况下发出相同的指令并让运行时处理它。
来自 MSDN:
If thisType
is a value type and thisType
implements method
then ptr
is passed unmodified as the this
pointer to a call
method instruction, for the implementation of method by thisType
.
并且:
The constrained
opcode allows IL compilers to make a call to a virtual function in a uniform way independent of whether ptr
is a value type or a reference type. Although it is intended for the case where thisType
is a generic type variable, the constrained prefix also works for nongeneric types and can reduce the complexity of generating virtual calls in languages that hide the distinction between value types and reference types.
我不知道有任何关于优化的官方文档,但您可以在 Roslyn 存储库中查看 MayUseCallForStructMethod
method.
的备注
至于为什么这种优化被推迟到非原始类型的运行时,我相信这是因为实现可以改变。想象一下,引用一个最初具有 ToString
覆盖的库,然后将 DLL(无需重新编译!)更改为删除覆盖的库。这会导致运行时异常。对于原语,他们可以确定它不会发生。
对于以下代码片段:
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
?
因为Int
是框架提供的密封类型,不会出现其他类型覆盖intToString
方法的情况,所以编译器知道它总是需要调用ToString()
方法实现在 int
类型中提供,因此不需要使用 callvirt
来确定调用哪个实现。
对于原始类型,编译器知道要调用 ToString
的哪个实现,但是当我们创建一个自定义值类型时,它是一个新的,它以前不存在,所以编译器不知道它它需要弄清楚要调用哪个实现以及它所在的位置,因为它默认继承自 Object
,因此编译器必须执行 callvirt
来定位提供的 ToString()
实现对于自定义类型,如果未覆盖,它将调用显而易见的对象类型。
以下现有的 SO 帖子可以帮助您理解这一点:
Call and Callvirt
这是编译器针对基本类型所做的优化。
但即使对于自定义结构,由于 constrained.
操作码,callvirt
实际上会在运行时作为 call
执行 - 在方法被覆盖的情况下。它允许编译器在任何一种情况下发出相同的指令并让运行时处理它。
来自 MSDN:
If
thisType
is a value type andthisType
implementsmethod
thenptr
is passed unmodified as thethis
pointer to acall
method instruction, for the implementation of method bythisType
.
并且:
The
constrained
opcode allows IL compilers to make a call to a virtual function in a uniform way independent of whetherptr
is a value type or a reference type. Although it is intended for the case wherethisType
is a generic type variable, the constrained prefix also works for nongeneric types and can reduce the complexity of generating virtual calls in languages that hide the distinction between value types and reference types.
我不知道有任何关于优化的官方文档,但您可以在 Roslyn 存储库中查看 MayUseCallForStructMethod
method.
至于为什么这种优化被推迟到非原始类型的运行时,我相信这是因为实现可以改变。想象一下,引用一个最初具有 ToString
覆盖的库,然后将 DLL(无需重新编译!)更改为删除覆盖的库。这会导致运行时异常。对于原语,他们可以确定它不会发生。