Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between call instance vs newobj instance in IL

Tags:

c#

il

I'm delving into C# in Depth, and playing with nullable value types. Just for experimental purposes I wrote a piece of code:

    private static void HowNullableWorks()
    {
        int test = 3;
        int? implicitConversion = test;
        Nullable<int> test2 = new Nullable<int>(3);

        MethodThatTakesNullableInt(null);
        MethodThatTakesNullableInt(39);
    }

And I was supprised to see that implicitConversion / test2 variables are initialized with:

call       instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)

instruction, whereas when MethodThatTakesNullableInt is called I can see:

IL_0017:  initobj    valuetype [mscorlib]System.Nullable`1<int32>

and

IL_0026:  newobj     instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)

which I understand. I thought that I'll see newobj instruction for implicitConversion / test2 as well.

This is full IL code:

.method private hidebysig static void  HowNullableWorks() cil managed
{
  // Code size       50 (0x32)
  .maxstack  2
  .locals init ([0] int32 test,
           [1] valuetype [mscorlib]System.Nullable`1<int32> implicitConversion,
           [2] valuetype [mscorlib]System.Nullable`1<int32> test2,
           [3] valuetype [mscorlib]System.Nullable`1<int32> CS$0$0000)
  IL_0000:  nop
  IL_0001:  ldc.i4.3
  IL_0002:  stloc.0
  IL_0003:  ldloca.s   implicitConversion
  IL_0005:  ldloc.0
  IL_0006:  call       instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
  IL_000b:  nop
  IL_000c:  ldloca.s   test2
  IL_000e:  ldc.i4.3
  IL_000f:  call       instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
  IL_0014:  nop
  IL_0015:  ldloca.s   CS$0$0000
  IL_0017:  initobj    valuetype [mscorlib]System.Nullable`1<int32>
  IL_001d:  ldloc.3
  IL_001e:  call       void csharp.in.depth._2nd.Program::MethodThatTakesNullableInt(valuetype [mscorlib]System.Nullable`1<int32>)
  IL_0023:  nop
  IL_0024:  ldc.i4.s   39
  IL_0026:  newobj     instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
  IL_002b:  call       void csharp.in.depth._2nd.Program::MethodThatTakesNullableInt(valuetype [mscorlib]System.Nullable`1<int32>)
  IL_0030:  nop
  IL_0031:  ret
} // end of method Program::HowNullableWorks
like image 761
dragonfly Avatar asked Aug 15 '12 09:08

dragonfly


1 Answers

First of all, it looks like you've compiled in Debug mode (based on the nops) - it's possible that you'll see different code emitted if you compile in Release mode.

Section I.12.1.6.2.1 of the ECMA CLR spec (Initializing instances of value types) says:

There are three options for initializing the home of a value type instance. You can zero it by loading the address of the home (see Table I.8: Address and Type of Home Locations) and using the initobj instruction (for local variables this is also accomplished by setting the localsinit bit in the method’s header). You can call a user-defined constructor by loading the address of the home (see Table I.8: Address and Type of Home Locations) and then calling the constructor directly. Or you can copy an existing instance into the home, as described in §I.12.1.6.2.2.

The first three uses of nullable types in your code result in null values stored in locals, so this comment is relevant (locals are one type of home for values): the first two are the locals implicitConversion and test that you've declared, and the third is a compiler-generated temporary called CS$0$0000. As the ECMA spec indicates, these locals can be initialized by using initobj (which is equivalent to the default no-args constructor for a struct, and is used for CS$0$0000 in this case) or by loading the local's address and calling a constructor (used for the other two locals).

However, for the final nullable instance (created by the implicit conversion from 39), the result is not stored in a local - it's generated on the stack, so the rules for initializing a home don't apply here. Instead, the compiler just uses newobj to create the value on the stack (as it would for any value or reference type).

You may be wondering why the compiler generated a local for the call to MethodThatTakesNullableInt(null) but not for MethodThatTakesNullableInt(39). I suspect that the answer is that the compiler always uses initobj to call the default constructor (which then requires a local or other home for the value), but uses newobj to call other constructors and store the result on the stack when there's not already an appropriate home for the value.

For more information, see also this comment from Section III.4.21 (newobj) from the spec:

Value types are not usually created using newobj. They are usually allocated either as arguments or local variables, using newarr (for zero-based, one-dimensional arrays), or as fields of objects. Once allocated, they are initialized using initobj. However, the newobj instruction can be used to create a new instance of a value type on the stack, that can then be passed as an argument, stored in a local, etc.

like image 115
kvb Avatar answered Oct 22 '22 08:10

kvb