Reputation: 12869
For the following code snippet:
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();
}
}
Compiler emits the following 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
Since both value type Test
and Int32
override the ToString()
method, I think no boxing will occur in both a.ToString()
and b.ToString()
. Thus I wonder why compiler emits constraned
+callvirt
for Test
, and call
for Int32
?
Upvotes: 4
Views: 329
Reputation: 22749
This is an optimization done by the compiler for primitive types.
But even for custom structs, callvirt
will actually be executed as call
at runtime due to the constrained.
opcode - in the case where the method was overridden. It allows the compiler to emit the same instructions in either case and let the runtime handle it.
From 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
.
And:
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.
I don't know of any official documentation for the optimization, but you can see the remarks in the Roslyn repo for the MayUseCallForStructMethod
method.
As to why this optimization is deferred to the runtime for non-primitive types, I believe it's because the implementation can change. Imagine referencing a library that originally had an override for ToString
, then changing the DLL (without recompiling!) to one where the override is removed. This would've caused a runtime exception. For primitives they can be sure it won't happen.
Upvotes: 6
Reputation: 62498
It is because Int
is a framework provided sealed type and it will never happen that some other type overrides int ToString
method, so compiler knows that it always needs to call the ToString()
method implementation provided in the int
type, so it does not need to use callvirt
to figure out which implementation to call.
For primitve types compiler knows which implementation of ToString
is to be called, but when we create a custom value type, it is a new one it never existed before, so compiler don't know about it and it needs to figure out about the implementation which one to call and where it resides, as it inherits by default from Object
, so compiler has to do callvirt
to locate the ToString()
implementation provided for custom type if not overriden it will call the Object type which is obvious.
The following existing SO posts can help you in understanding this:
Upvotes: 0