Skip to content

Instantly share code, notes, and snippets.

@gfoidl
Last active January 22, 2018 10:00
Show Gist options
  • Save gfoidl/6decb36753b3e3098e899f9a0894e852 to your computer and use it in GitHub Desktop.
Save gfoidl/6decb36753b3e3098e899f9a0894e852 to your computer and use it in GitHub Desktop.
Struct argument copy & byref
using System;
using System.Runtime.CompilerServices;
namespace ConsoleApp2
{
class Program
{
static void Main(string[] args)
{
var foo1 = new Foo1(1, 2, 3);
Console.WriteLine(Length1(foo1));
foo1 = new Foo1(4, 5, 6);
Console.WriteLine(Length2(foo1));
foo1 = new Foo1(7, 8, 9);
Console.WriteLine(Length21(ref foo1));
var foo2 = new Foo2(1, 2, 3);
Console.WriteLine(Length3(foo2));
foo2 = new Foo2(4, 5, 6);
Console.WriteLine(Length4(foo2));
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static int Length1(Foo1 foo) => foo.X + foo.Y + foo.Z;
[MethodImpl(MethodImplOptions.NoInlining)]
private static int Length2(in Foo1 foo) => foo.X + foo.Y + foo.Z;
private static int Length21(ref Foo1 foo) => foo.X + foo.Y + foo.Z;
[MethodImpl(MethodImplOptions.NoInlining)]
private static int Length3(Foo2 foo) => foo.X + foo.Y + foo.Z;
[MethodImpl(MethodImplOptions.NoInlining)]
private static int Length4(in Foo2 foo) => foo.X + foo.Y + foo.Z;
}
public struct Foo1
{
public int X { get; }
public int Y { get; }
public int Z { get; }
public Foo1(int x, int y, int z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
}
public readonly struct Foo2
{
public int X { get; }
public int Y { get; }
public int Z { get; }
public Foo2(int x, int y, int z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
}
}
.class private auto ansi beforefieldinit ConsoleApp2.Program
extends [System.Runtime]System.Object
{
// Methods
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 110 (0x6e)
.maxstack 4
.entrypoint
.locals init (
[0] valuetype ConsoleApp2.Foo1,
[1] valuetype ConsoleApp2.Foo2
)
IL_0000: ldloca.s 0
IL_0002: ldc.i4.1
IL_0003: ldc.i4.2
IL_0004: ldc.i4.3
IL_0005: call instance void ConsoleApp2.Foo1::.ctor(int32, int32, int32)
IL_000a: ldloc.0
IL_000b: call int32 ConsoleApp2.Program::Length1(valuetype ConsoleApp2.Foo1)
IL_0010: call void [System.Console]System.Console::WriteLine(int32)
IL_0015: ldloca.s 0
IL_0017: ldc.i4.4
IL_0018: ldc.i4.5
IL_0019: ldc.i4.6
IL_001a: call instance void ConsoleApp2.Foo1::.ctor(int32, int32, int32)
IL_001f: ldloca.s 0
IL_0021: call int32 ConsoleApp2.Program::Length2(valuetype ConsoleApp2.Foo1&)
IL_0026: call void [System.Console]System.Console::WriteLine(int32)
IL_002b: ldloca.s 0
IL_002d: ldc.i4.7
IL_002e: ldc.i4.8
IL_002f: ldc.i4.s 9
IL_0031: call instance void ConsoleApp2.Foo1::.ctor(int32, int32, int32)
IL_0036: ldloca.s 0
IL_0038: call int32 ConsoleApp2.Program::Length21(valuetype ConsoleApp2.Foo1&)
IL_003d: call void [System.Console]System.Console::WriteLine(int32)
IL_0042: ldloca.s 1
IL_0044: ldc.i4.1
IL_0045: ldc.i4.2
IL_0046: ldc.i4.3
IL_0047: call instance void ConsoleApp2.Foo2::.ctor(int32, int32, int32)
IL_004c: ldloc.1
IL_004d: call int32 ConsoleApp2.Program::Length3(valuetype ConsoleApp2.Foo2)
IL_0052: call void [System.Console]System.Console::WriteLine(int32)
IL_0057: ldloca.s 1
IL_0059: ldc.i4.4
IL_005a: ldc.i4.5
IL_005b: ldc.i4.6
IL_005c: call instance void ConsoleApp2.Foo2::.ctor(int32, int32, int32)
IL_0061: ldloca.s 1
IL_0063: call int32 ConsoleApp2.Program::Length4(valuetype ConsoleApp2.Foo2&)
IL_0068: call void [System.Console]System.Console::WriteLine(int32)
IL_006d: ret
} // end of method Program::Main
.method private hidebysig static
int32 Length1 (
valuetype ConsoleApp2.Foo1 foo
) cil managed noinlining
{
// Method begins at RVA 0x20ca
// Code size 24 (0x18)
.maxstack 8
IL_0000: ldarga.s foo
IL_0002: call instance int32 ConsoleApp2.Foo1::get_X()
IL_0007: ldarga.s foo
IL_0009: call instance int32 ConsoleApp2.Foo1::get_Y()
IL_000e: add
IL_000f: ldarga.s foo
IL_0011: call instance int32 ConsoleApp2.Foo1::get_Z()
IL_0016: add
IL_0017: ret
} // end of method Program::Length1
.method private hidebysig static
int32 Length2 (
valuetype ConsoleApp2.Foo1& foo
) cil managed noinlining
{
.param [1]
.custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x20e4
// Code size 45 (0x2d)
.maxstack 2
.locals init (
[0] valuetype ConsoleApp2.Foo1
)
IL_0000: ldarg.0
IL_0001: ldobj ConsoleApp2.Foo1
IL_0006: stloc.0
IL_0007: ldloca.s 0
IL_0009: call instance int32 ConsoleApp2.Foo1::get_X()
IL_000e: ldarg.0
IL_000f: ldobj ConsoleApp2.Foo1
IL_0014: stloc.0
IL_0015: ldloca.s 0
IL_0017: call instance int32 ConsoleApp2.Foo1::get_Y()
IL_001c: add
IL_001d: ldarg.0
IL_001e: ldobj ConsoleApp2.Foo1
IL_0023: stloc.0
IL_0024: ldloca.s 0
IL_0026: call instance int32 ConsoleApp2.Foo1::get_Z()
IL_002b: add
IL_002c: ret
} // end of method Program::Length2
.method private hidebysig static
int32 Length21 (
valuetype ConsoleApp2.Foo1& foo
) cil managed
{
// Method begins at RVA 0x211d
// Code size 21 (0x15)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance int32 ConsoleApp2.Foo1::get_X()
IL_0006: ldarg.0
IL_0007: call instance int32 ConsoleApp2.Foo1::get_Y()
IL_000c: add
IL_000d: ldarg.0
IL_000e: call instance int32 ConsoleApp2.Foo1::get_Z()
IL_0013: add
IL_0014: ret
} // end of method Program::Length21
.method private hidebysig static
int32 Length3 (
valuetype ConsoleApp2.Foo2 foo
) cil managed noinlining
{
// Method begins at RVA 0x2133
// Code size 24 (0x18)
.maxstack 8
IL_0000: ldarga.s foo
IL_0002: call instance int32 ConsoleApp2.Foo2::get_X()
IL_0007: ldarga.s foo
IL_0009: call instance int32 ConsoleApp2.Foo2::get_Y()
IL_000e: add
IL_000f: ldarga.s foo
IL_0011: call instance int32 ConsoleApp2.Foo2::get_Z()
IL_0016: add
IL_0017: ret
} // end of method Program::Length3
.method private hidebysig static
int32 Length4 (
valuetype ConsoleApp2.Foo2& foo
) cil managed noinlining
{
.param [1]
.custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x214c
// Code size 21 (0x15)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance int32 ConsoleApp2.Foo2::get_X()
IL_0006: ldarg.0
IL_0007: call instance int32 ConsoleApp2.Foo2::get_Y()
IL_000c: add
IL_000d: ldarg.0
IL_000e: call instance int32 ConsoleApp2.Foo2::get_Z()
IL_0013: add
IL_0014: ret
} // end of method Program::Length4
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// Method begins at RVA 0x2162
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: ret
} // end of method Program::.ctor
} // end of class ConsoleApp2.Program
; Assembly listing for method ConsoleApp2.Program:Main(ref)
; Emitting BLENDED_CODE for X64 CPU with AVX
; optimized code
; rbp based frame
; partially interruptible
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) ref -> zero-ref class-hnd
; V01 loc0 [V01 ] ( 14, 14 ) struct (16) [rbp-0x18] do-not-enreg[XS] must-init addr-exposed ld-addr-op
; V02 loc1 [V02 ] ( 10, 10 ) struct (16) [rbp-0x28] do-not-enreg[XS] must-init addr-exposed ld-addr-op
; V03 tmp0 [V03 ] ( 6, 6 ) int -> [rbp-0x18] do-not-enreg[X] addr-exposed V01.<X>k__BackingField(offs=0x00) P-DEP
; V04 tmp1 [V04 ] ( 6, 6 ) int -> [rbp-0x14] do-not-enreg[X] addr-exposed V01.<Y>k__BackingField(offs=0x04) P-DEP
; V05 tmp2 [V05 ] ( 6, 6 ) int -> [rbp-0x10] do-not-enreg[X] addr-exposed V01.<Z>k__BackingField(offs=0x08) P-DEP
; V06 tmp3 [V06 ] ( 4, 4 ) int -> [rbp-0x28] do-not-enreg[X] addr-exposed V02.<X>k__BackingField(offs=0x00) P-DEP
; V07 tmp4 [V07 ] ( 4, 4 ) int -> [rbp-0x24] do-not-enreg[X] addr-exposed V02.<Y>k__BackingField(offs=0x04) P-DEP
; V08 tmp5 [V08 ] ( 4, 4 ) int -> [rbp-0x20] do-not-enreg[X] addr-exposed V02.<Z>k__BackingField(offs=0x08) P-DEP
; V09 tmp6 [V09 ] ( 4, 8 ) struct (16) [rbp-0x38] do-not-enreg[XSB] addr-exposed
; V10 tmp7 [V10,T00] ( 4, 8 ) byref -> rdi stack-byref
; V11 tmp8 [V11 ] ( 4, 8 ) struct (16) [rbp-0x48] do-not-enreg[XSB] addr-exposed
; V12 tmp9 [V12,T01] ( 4, 8 ) byref -> rdi stack-byref
;# V13 OutArgs [V13 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00]
;
; Lcl frame size = 72
G_M36834_IG01:
55 push rbp
4155 push r13
4883EC48 sub rsp, 72
488D6C2450 lea rbp, [rsp+50H]
4C8BEF mov r13, rdi
488D7DD8 lea rdi, [rbp-28H]
B908000000 mov ecx, 8
33C0 xor rax, rax
F3AB rep stosd
498BFD mov rdi, r13
G_M36834_IG02:
C745E801000000 mov dword ptr [rbp-18H], 1 ; var foo1 = new Foo1(1, 2, 3);
C745EC02000000 mov dword ptr [rbp-14H], 2
C745F003000000 mov dword ptr [rbp-10H], 3
488D7DC8 lea rdi, bword ptr [rbp-38H] ; begin copy of struct
8B75E8 mov esi, dword ptr [rbp-18H]
8937 mov dword ptr [rdi], esi
8B75EC mov esi, dword ptr [rbp-14H]
897704 mov dword ptr [rdi+4], esi
8B75F0 mov esi, dword ptr [rbp-10H]
897708 mov dword ptr [rdi+8], esi ; end copy of struct
488B7DC8 mov rdi, qword ptr [rbp-38H]
8B75D0 mov esi, dword ptr [rbp-30H]
E87BFAFFFF call ConsoleApp2.Program:Length1(struct):int
8BF8 mov edi, eax
E834FBFFFF call System.Console:WriteLine(int)
C745E804000000 mov dword ptr [rbp-18H], 4 ; foo1 = new Foo1(4, 5, 6);
C745EC05000000 mov dword ptr [rbp-14H], 5
C745F006000000 mov dword ptr [rbp-10H], 6
488D7DE8 lea rdi, bword ptr [rbp-18H] ; note no copy
E85EFAFFFF call ConsoleApp2.Program:Length2(byref):int
8BF8 mov edi, eax
E80FFBFFFF call System.Console:WriteLine(int)
C745E807000000 mov dword ptr [rbp-18H], 7 ; foo1 = new Foo1(7, 8, 9);
C745EC08000000 mov dword ptr [rbp-14H], 8
C745F009000000 mov dword ptr [rbp-10H], 9
488D7DE8 lea rdi, bword ptr [rbp-18H] ; note no copy
E841FAFFFF call ConsoleApp2.Program:Length21(byref):int
8BF8 mov edi, eax
E8EAFAFFFF call System.Console:WriteLine(int)
C745D801000000 mov dword ptr [rbp-28H], 1 ; var foo2 = new Foo2(1, 2, 3);
C745DC02000000 mov dword ptr [rbp-24H], 2
C745E003000000 mov dword ptr [rbp-20H], 3
488D7DB8 lea rdi, bword ptr [rbp-48H] ; begin copy of struct
8B75D8 mov esi, dword ptr [rbp-28H]
8937 mov dword ptr [rdi], esi
8B75DC mov esi, dword ptr [rbp-24H]
897704 mov dword ptr [rdi+4], esi
8B75E0 mov esi, dword ptr [rbp-20H]
897708 mov dword ptr [rdi+8], esi ; end copy of struct
488B7DB8 mov rdi, qword ptr [rbp-48H]
8B75C0 mov esi, dword ptr [rbp-40H]
E80CFAFFFF call ConsoleApp2.Program:Length3(struct):int
8BF8 mov edi, eax
E8ADFAFFFF call System.Console:WriteLine(int)
C745D804000000 mov dword ptr [rbp-28H], 4
C745DC05000000 mov dword ptr [rbp-24H], 5
C745E006000000 mov dword ptr [rbp-20H], 6
488D7DD8 lea rdi, bword ptr [rbp-28H] ; note no copy
E8EFF9FFFF call ConsoleApp2.Program:Length4(byref):int
8BF8 mov edi, eax
E888FAFFFF call System.Console:WriteLine(int)
90 nop
G_M36834_IG03:
488D65F8 lea rsp, [rbp-08H]
415D pop r13
5D pop rbp
C3 ret
; Total bytes of code 273, prolog size 31 for method ConsoleApp2.Program:Main(ref)
; ============================================================
; Assembly listing for method ConsoleApp2.Program:Length1(struct):int
; Emitting BLENDED_CODE for X64 CPU with AVX
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
; V00 arg0 [V00,T00] ( 5, 5 ) struct (16) [rsp+0x08] do-not-enreg[SF] ld-addr-op
; V01 tmp0 [V01,T01] ( 2, 4 ) int -> rax
; V02 tmp1 [V02,T02] ( 2, 4 ) int -> rax
;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00]
;
; Lcl frame size = 24
G_M42526_IG01:
4883EC18 sub rsp, 24
48897C2408 mov qword ptr [rsp+08H], rdi
89742410 mov dword ptr [rsp+10H], esi
G_M42526_IG02:
8B442408 mov eax, dword ptr [rsp+08H]
0344240C add eax, dword ptr [rsp+0CH]
03442410 add eax, dword ptr [rsp+10H]
G_M42526_IG03:
4883C418 add rsp, 24
C3 ret
; Total bytes of code 30, prolog size 4 for method ConsoleApp2.Program:Length1(struct):int
; ============================================================
; Assembly listing for method ConsoleApp2.Program:Length2(byref):int
; Emitting BLENDED_CODE for X64 CPU with AVX
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
; V00 arg0 [V00,T00] ( 20, 20 ) byref -> rdi
;* V01 loc0 [V01 ] ( 0, 0 ) struct (16) zero-ref ld-addr-op
; V02 tmp0 [V02,T07] ( 2, 4 ) int -> rax
; V03 tmp1 [V03,T08] ( 2, 4 ) int -> rax
; V04 tmp2 [V04,T04] ( 5, 5 ) int -> rax V01.<X>k__BackingField(offs=0x00) P-INDEP
; V05 tmp3 [V05,T05] ( 5, 5 ) int -> rsi V01.<Y>k__BackingField(offs=0x04) P-INDEP
; V06 tmp4 [V06,T06] ( 5, 5 ) int -> rdi V01.<Z>k__BackingField(offs=0x08) P-INDEP
;# V07 OutArgs [V07 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00]
; V08 cse0 [V08,T01] ( 14, 14 ) int -> rax
; V09 cse1 [V09,T02] ( 10, 10 ) int -> rsi
; V10 cse2 [V10,T03] ( 6, 6 ) int -> rdi
;
; Lcl frame size = 0
G_M42523_IG01:
G_M42523_IG02:
8B07 mov eax, dword ptr [rdi] ; copy to registers
815B7704 mov esi, dword ptr [rdi+4]
8B7F08 mov edi, dword ptr [rdi+8] ; end copy to registers
03C6 add eax, esi
03C7 add eax, edi
G_M42523_IG03:
C3 ret
; Total bytes of code 13, prolog size 0 for method ConsoleApp2.Program:Length2(byref):int
; ============================================================
; Assembly listing for method ConsoleApp2.Program:Length21(byref):int
; Emitting BLENDED_CODE for X64 CPU with AVX
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
; V00 arg0 [V00,T00] ( 5, 5 ) byref -> rdi
; V01 tmp0 [V01,T01] ( 2, 4 ) int -> rax
; V02 tmp1 [V02,T02] ( 2, 4 ) int -> rax
;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00]
;
; Lcl frame size = 0
G_M60300_IG01:
G_M60300_IG02:
8B07 mov eax, dword ptr [rdi] ; note no copy
034704 add eax, dword ptr [rdi+4]
034708 add eax, dword ptr [rdi+8]
G_M60300_IG03:
C3 ret
; Total bytes of code 9, prolog size 0 for method ConsoleApp2.Program:Length21(byref):int
; ============================================================
; Assembly listing for method ConsoleApp2.Program:Length3(struct):int
; Emitting BLENDED_CODE for X64 CPU with AVX
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
; V00 arg0 [V00,T00] ( 5, 5 ) struct (16) [rsp+0x08] do-not-enreg[SF] ld-addr-op
; V01 tmp0 [V01,T01] ( 2, 4 ) int -> rax
; V02 tmp1 [V02,T02] ( 2, 4 ) int -> rax
;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00]
;
; Lcl frame size = 24
G_M42524_IG01:
4883EC18 sub rsp, 24
48897C2408 mov qword ptr [rsp+08H], rdi
89742410 mov dword ptr [rsp+10H], esi
G_M42524_IG02:
8B442408 mov eax, dword ptr [rsp+08H]
0344240C add eax, dword ptr [rsp+0CH]
03442410 add eax, dword ptr [rsp+10H]
G_M42524_IG03:
4883C418 add rsp, 24
C3 ret
; Total bytes of code 30, prolog size 4 for method ConsoleApp2.Program:Length3(struct):int
; ============================================================
; Assembly listing for method ConsoleApp2.Program:Length4(byref):int
; Emitting BLENDED_CODE for X64 CPU with AVX
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
; V00 arg0 [V00,T00] ( 5, 5 ) byref -> rdi
; V01 tmp0 [V01,T01] ( 2, 4 ) int -> rax
; V02 tmp1 [V02,T02] ( 2, 4 ) int -> rax
;# V03 OutArgs [V03 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00]
;
; Lcl frame size = 0
G_M42529_IG01:
G_M42529_IG02:
8B07 mov eax, dword ptr [rdi] ; note no copy
034704 add eax, dword ptr [rdi+4]
034708 add eax, dword ptr [rdi+8]
G_M42529_IG03:
C3 ret
; Total bytes of code 9, prolog size 0 for method ConsoleApp2.Program:Length4(byref):int
; ============================================================
@gfoidl
Copy link
Author

gfoidl commented Jan 22, 2018

Discussion

Method with argument by value

Length1 and Length3 produce the same code. The caller makes a copy of the struct, and this copy is passed to the method. The method can assume this copy is immutable (in the sense that the copy is thrown away when the method goes out of scope), hence can operate directly on the values.

Method with argument by reference

ref

Length21 doesn't need a copy, because the argument is passed by reference. The compiler can assume a mutable struct, and can operate direct on the values.

in

in means "readonly ref", so the compiler has to distinguish two cases: mutable and immutable (readonly) structs.

Mutable structs

For mutable structs the compiler has to assume that the struct is mutated, which is not allowed by in, so it has to make a copy of the struct. This copy is not done an the callsite, but on the callee -- can be clearly seen in the IL and dasm.
The code is equivalent to:

private static int Length2(in Foo1 foo)
{
    Foo1 foo2 = foo;
    int x     = foo2.X;
    foo2      = foo;
    int num   = x + foo2.Y;
    foo2      = foo;
    return num + foo2.Z;
}

Immutable (readonly) structs

In this case the compiler knows that the struct can't be mutated, hence it can produce optimized code without the need for any copy.

Benchmarks

BenchmarkDotNet=v0.10.12, OS=Windows 10 Redstone 3 [1709, Fall Creators Update] (10.0.16299.125)
Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical cores and 4 physical cores
Frequency=2742191 Hz, Resolution=364.6719 ns, Timer=TSC
.NET Core SDK=2.1.4
  [Host]     : .NET Core 2.0.5 (Framework 4.6.26020.03), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.5 (Framework 4.6.26020.03), 64bit RyuJIT

Method Mean Error StdDev Scaled ScaledSD
Length1 1.618 ns 0.0262 ns 0.0245 ns 1.00 0.00
Length2 1.209 ns 0.0209 ns 0.0196 ns 0.75 0.02
Length21 1.173 ns 0.0104 ns 0.0092 ns 0.72 0.01
Length3 1.668 ns 0.0604 ns 0.0504 ns 1.03 0.03
Lenght4 1.170 ns 0.0167 ns 0.0156 ns 0.72 0.01

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment