This is the nineteenth part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#
Today we will see that .NET can create an instance of a structure without calling its constructor if it is not needed.
Let’s take this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public struct SomeStructure { public int Value { get; set; } public SomeStructure(int value) { Value = value; } public int Foo() { return new SomeStructure(123).Value; } public int Bar() { var s = new SomeStructure(456); return s.Value; } } |
Structure with integer field. Then, two methods creating a structure and returning its field. The difference is first method returns directly, second method stores in a variable. Let’s see IL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.method public hidebysig instance int32 Foo () cil managed { // Method begins at RVA 0x206c // Code size 16 (0x10) .maxstack 1 .locals init ( [0] valuetype SomeStructure ) IL_0000: ldc.i4.s 123 IL_0002: newobj instance void SomeStructure::.ctor(int32) IL_0007: stloc.0 IL_0008: ldloca.s 0 IL_000a: call instance int32 SomeStructure::get_Value() IL_000f: ret } // end of method SomeStructure::Foo |
We can see that we create an instance using newobj
. Second method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.method public hidebysig instance int32 Bar () cil managed { // Method begins at RVA 0x2088 // Code size 20 (0x14) .maxstack 2 .locals init ( [0] valuetype SomeStructure ) IL_0000: ldloca.s 0 IL_0002: ldc.i4 456 IL_0007: call instance void SomeStructure::.ctor(int32) IL_000c: ldloca.s 0 IL_000e: call instance int32 SomeStructure::get_Value() IL_0013: ret } // end of method SomeStructure::Bar |
And magic, this time we don’t create an instance, we just call the constructor. This is optimization on the IL level. If we check jitted code in debug mode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
SomeStructure.Foo() L0000: push ebp L0001: mov ebp, esp L0003: sub esp, 0x14 L0006: xor eax, eax L0008: mov [ebp-0x8], eax L000b: mov [ebp-0xc], eax L000e: mov [ebp-0x4], ecx L0011: cmp dword [0x13ebc1a8], 0x0 L0018: jz L001f L001a: call 0x726310f0 L001f: nop L0020: xor ecx, ecx L0022: mov [ebp-0x10], ecx L0025: lea ecx, [ebp-0x10] L0028: mov edx, 0x7b L002d: call SomeStructure..ctor(Int32) L0032: mov ecx, [ebp-0x10] L0035: mov [ebp-0x8], ecx L0038: lea ecx, [ebp-0x8] L003b: call SomeStructure.get_Value() L0040: mov [ebp-0x14], eax L0043: mov eax, [ebp-0x14] L0046: mov [ebp-0xc], eax L0049: nop L004a: jmp L004c L004c: mov eax, [ebp-0xc] L004f: mov esp, ebp L0051: pop ebp L0052: ret |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
SomeStructure.Bar() L0000: push ebp L0001: mov ebp, esp L0003: sub esp, 0x10 L0006: xor eax, eax L0008: mov [ebp-0x8], eax L000b: mov [ebp-0xc], eax L000e: mov [ebp-0x4], ecx L0011: cmp dword [0x13ebc1a8], 0x0 L0018: jz L001f L001a: call 0x726310f0 L001f: nop L0020: lea ecx, [ebp-0x8] L0023: mov edx, 0x1c8 L0028: call SomeStructure..ctor(Int32) L002d: lea ecx, [ebp-0x8] L0030: call SomeStructure.get_Value() L0035: mov [ebp-0x10], eax L0038: mov eax, [ebp-0x10] L003b: mov [ebp-0xc], eax L003e: nop L003f: jmp L0041 L0041: mov eax, [ebp-0xc] L0044: mov esp, ebp L0046: pop ebp L0047: ret |
We can see there are differences (reflecting differences in IL). However, in Release:
1 2 3 4 5 6 7 |
SomeStructure.Foo() L0000: mov eax, 0x7b L0005: ret SomeStructure.Bar() L0000: mov eax, 0x1c8 L0005: ret |
It works in the same way.