4

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?

Ehsan Sajjad
  • 61,834
  • 16
  • 105
  • 160
Lifu Huang
  • 11,930
  • 14
  • 55
  • 77

2 Answers2

6

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 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.

And:

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.

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.

Eli Arbel
  • 22,391
  • 3
  • 45
  • 71
  • Thanks a lot, I also guess it is the compiler optimization working. But I cannot find any material supporting my guess. So I would appreciate it if you could provide any docs about this special optimization for primitive types. Thanks again. – Lifu Huang Jul 30 '16 at 17:05
  • Thank you @Eli Arbel , you really solved my question : ) – Lifu Huang Jul 31 '16 at 02:54
0

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:

Call and Callvirt

Community
  • 1
  • 1
Ehsan Sajjad
  • 61,834
  • 16
  • 105
  • 160
  • 1
    But all value types are implicitly sealed, so I think from this point of view, `Int32` and `Test` should behave in the same way. Please correct me if I am wrong. – Lifu Huang Jul 30 '16 at 16:50
  • yes it is framework provided sealed type, that is special case – Ehsan Sajjad Jul 30 '16 at 16:51
  • Sorry, I don't know what your "special case" means here. Would you please give me more details? – Lifu Huang Jul 30 '16 at 16:55
  • as @ali said compiler do optimization for performance of code execution as it can for primitive types which cannot be extended – Ehsan Sajjad Jul 30 '16 at 16:57
  • makes sense, for custom types it happens on runtime, and for primitive types on compile time, right @EliArbel ? – Ehsan Sajjad Jul 30 '16 at 17:06
  • Hi @EliArbel, since custom value type is known to be sealed at compile time. I am curious why should the optimization be delayed to runtime, instead of compile time? Thanks a lot! – Lifu Huang Jul 30 '16 at 17:10
  • @LifuHuang it is decided on runtime which implementation of ToString to be called in custom value type, as there are 2 possibilities, you overrided ToString, or you did'nt, so it needs to figure out on run time, what i think – Ehsan Sajjad Jul 30 '16 at 17:13
  • 1
    "compiler don't know about it" The compiler certainly has all the information to figure out that the override exists and it could use `call`. It just does decides not to, for the reason explained at the end of @EliArbel's answer. – svick Jul 30 '16 at 17:37