From C# to CLR Jitted Code - ByVal and ByRef

I am trying to understand the difference between ByVal and ByRef objects. In below C# code we pass different parameter types to the Test method. Let’s see how the runtime treat them differently at the IL and Assembly level.

using System;

using System.Runtime.CompilerServices;

namespace CodeGen

{

    public class MyClass

    {

        public static void Main()

        {

            MyType o1 = new MyType();

            MyType o2 = new MyType();

            MyType o3;

            Test(o1, ref o2, out o3);

        }

        [MethodImpl(MethodImplOptions.NoInlining)] // Without this attribute, it will be inlined

        public static void Test(MyType o1, ref MyType o2, out MyType o3)

        {

            o1.X = 1;

            o2.X = 2;

            o3 = new MyType();

            o3.X = 3;

        }

    }

    public class MyType

    {

        public int X;

    }

}

I highlighted operation that is specific for ByRef object. ByVal object usually can be pushed or popped from stack by one IL opcode. ByRef object is usually accessed indirectly from a managed pointer. That is why we need those stind ldind opcode.

 

.method public hidebysig static void Main() cil managed

{

  .entrypoint

  // Code size 25 (0x19)

  .maxstack 3

  .locals init (class CodeGen.MyType V_0,

           class CodeGen.MyType V_1,

           class CodeGen.MyType V_2)

  IL_0000: nop

  IL_0001: newobj instance void CodeGen.MyType::.ctor()

  IL_0006: stloc.0

  IL_0007: newobj instance void CodeGen.MyType::.ctor()

  IL_000c: stloc.1

  IL_000d: ldloc.0

  IL_000e: ldloca.s V_1

  IL_0010: ldloca.s V_2

  IL_0012: call void CodeGen.MyClass::Test(class CodeGen.MyType,

                                                  class CodeGen.MyType&,

                                                  class CodeGen.MyType&)

  IL_0017: nop

  IL_0018: ret

} // end of method MyClass::Main

.method public hidebysig static void Test(class CodeGen.MyType o1,

                                           class CodeGen.MyType& o2,

                                           [out] class CodeGen.MyType& o3) cil managed noinlining

{

  // Code size 32 (0x20)

  .maxstack 8

  IL_0000: nop

  IL_0001: ldarg.0

  IL_0002: ldc.i4.1

  IL_0003: stfld int32 CodeGen.MyType::X

  IL_0008: ldarg.1

  IL_0009: ldind.ref

  IL_000a: ldc.i4.2

  IL_000b: stfld int32 CodeGen.MyType::X

  IL_0010: ldarg.2 //It seems that below code can be optimized by reorder the stind.ref to the end of the method.

  IL_0011: newobj instance void CodeGen.MyType::.ctor()

  IL_0016: stind.ref

  IL_0017: ldarg.2

  IL_0018: ldind.ref

  IL_0019: ldc.i4.3

  IL_001a: stfld int32 CodeGen.MyType::X

  IL_001f: ret

} // end of method MyClass::Test

CodeGen.MyClass.Main()

Begin 02570070, size 4e

02570070 57 push edi // Preserve edi esi, in case it is used in the caller code, since we are going to change both in below code

02570071 56 push esi

02570072 83ec08 sub esp,8 // Leave some stack space and use them as local variables

02570075 33c0 xor eax,eax

02570077 890424 mov dword ptr [esp],eax // set ref o3 to null

0257007a 89442404 mov dword ptr [esp+4],eax // set ref o2 to null

0257007e b97c34f301 mov ecx,1F3347Ch // !dumptype 1Fee47ch reveals that it is the MethodTable for CodeGen.MyType

02570083 e89c1f9bff call 01f22024 // Allocate some space for above type

02570088 8bf0 mov esi,eax // o1 -> esi !do eax can prove that

0257008a 8bce mov ecx,esi // ecx represents this pointer

0257008c e807440f59 call mscorlib_ni!System.Object..ctor() (5b664498) // Complete object construction

02570091 b97c34f301 mov ecx,1F3347Ch   

02570096 e8891f9bff call 01f22024                 

0257009b 8bf8 mov edi,eax // o2 ->edi

0257009d 8bcf mov ecx,edi

0257009f e8f4430f59 call mscorlib_ni!System.Object..ctor() (5b664498)

025700a4 893c24 mov dword ptr [esp],edi

025700a7 8d442404 lea eax,[esp+4] // ref o3 is still null

025700ab 50 push eax // push ref o3 so that Callee can pick it up

025700ac 8d542404 lea edx,[esp+4] // edx = [edi] ref o2

025700b0 8bce mov ecx,esi // o1 -> ecx

025700b2 ff15fc32f301 call dword ptr ds:[1F332FCh] // Stub for Test, since Test is not Jitted yet

025700b8 83c408 add esp,8 // clean up stack and restore esi edi

025700bb 5e pop esi

025700bc 5f pop edi

025700bd c3 ret

CodeGen.MyClass.Test(CodeGen.MyType, CodeGen.MyType ByRef, CodeGen.MyType ByRef)

025700d2 57 push edi

025700d3 56 push esi

025700d4 8b7c240c mov edi,dword ptr [esp+0Ch] // ref o3 -> edi

025700d8 c7410401000000 mov dword ptr [ecx+4],1 // o1 ->ecx +4 is the offset for the field. The first 4 bytes represents the method table.

025700df 8b02 mov eax,dword ptr [edx] // o2 ->[edx] o2->eax ByRef has to be accessed indirectly here

025700e1 c7400402000000 mov dword ptr [eax+4],2 // set field again

025700e8 b97c34f301 mov ecx,1F3347Ch //CodeGen.MyType)

025700ed e8321f9bff call 01f22024 // Allocate memory

025700f2 8bf0 mov esi,eax // o3 -> esi

025700f4 8bce mov ecx,esi

025700f6 e89d430f59 call mscorlib_ni!System.Object..ctor() (5b664498)

025700fb 8d17 lea edx,[edi] // This along with the WriteBarrier makes sure that GC can find the root of o3

025700fd e8eeaa5b5b call 5db2abf0 // JitHelper for WriteBarrier

02570102 8b07 mov eax,dword ptr [edi]

02570104 c7400403000000 mov dword ptr [eax+4],3 // set field for o3

0257010b 5e pop esi

0257010c 5f pop edi

0257010d c20400 ret 4