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