I was checking whether or not deconstruction causes extra objects to be instantiated on the heap since I am doing something in an area where I need to have as little GC pressure as possible. This is the code I was trying out:
using System;
public struct Pair
{
public int A;
public int B;
public Pair(int a, int b)
{
A = a;
B = b;
}
public void Deconstruct(out int a, out int b)
{
a = A;
b = B;
}
}
public class Program
{
public static void Main()
{
Pair pair = new Pair(1, 2);
// Line of interest
(int a, int b) = pair;
Console.WriteLine(a + " " + b);
}
}
I ran this through SharpLab to see what C# is doing for me, and it does the following:
public static void Main()
{
Pair pair = new Pair(1, 2);
Pair pair2 = pair;
int a;
int b;
pair2.Deconstruct(out a, out b);
int num = a;
int num2 = b;
Console.WriteLine(num.ToString() + " " + num2.ToString());
}
This answered my original question of whether or not I have to worry about extra allocations... but then even more interestingly, the release mode (since the above is debug) has:
public static void Main()
{
int a;
int b;
new Pair(1, 2).Deconstruct(out a, out b);
int num = a;
int num2 = b;
Console.WriteLine(num.ToString() + " " + num2.ToString());
}
However this can be reduced down to (this is me doing some extra variable pruning num
and num2
):
public static void Main()
{
int a;
int b;
new Pair(1, 2).Deconstruct(out a, out b);
Console.WriteLine(a.ToString() + " " + b.ToString());
}
This is a question of interest, since there is no way extra stack allocation of two integers is going to mean anything in terms of my program performance. For fun though I tried looking at the IL of Main
and get:
// Methods
.method public hidebysig static
void Main () cil managed
{
// Method begins at RVA 0x2074
// Code size 54 (0x36)
.maxstack 3
.locals init (
[0] int32,
[1] int32,
[2] valuetype Pair,
[3] int32,
[4] int32
)
IL_0000: ldc.i4.1
IL_0001: ldc.i4.2
IL_0002: newobj instance void Pair::.ctor(int32, int32)
IL_0007: stloc.2
IL_0008: ldloca.s 2
IL_000a: ldloca.s 3
IL_000c: ldloca.s 4
IL_000e: call instance void Pair::Deconstruct(int32&, int32&)
IL_0013: ldloc.3
IL_0014: stloc.0
IL_0015: ldloc.s 4
IL_0017: stloc.1
IL_0018: ldloca.s 0
IL_001a: call instance string [System.Private.CoreLib]System.Int32::ToString()
IL_001f: ldstr " "
IL_0024: ldloca.s 1
IL_0026: call instance string [System.Private.CoreLib]System.Int32::ToString()
IL_002b: call string [System.Private.CoreLib]System.String::Concat(string, string, string)
IL_0030: call void [System.Console]System.Console::WriteLine(string)
IL_0035: ret
} // end of method Program::Main
and the JIT ASM is
Program.Main()
L0000: push ebp
L0001: mov ebp, esp
L0003: push edi
L0004: push esi
L0005: push ebx
L0006: sub esp, 0x20
L0009: lea edi, [ebp-0x28]
L000c: call 0x68233ac
L0011: mov eax, ebp
L0013: mov [ebp-0x14], eax
L0016: push 0x3
L0018: mov dword [ebp-0x20], 0x6cce29c
L001f: mov eax, esp
L0021: mov [ebp-0x1c], eax
L0024: lea eax, [0x146004df]
L002a: mov [ebp-0x18], eax
L002d: mov byte [esi+0x8], 0x0
L0031: call dword [0x6cce680]
L0037: mov byte [esi+0x8], 0x1
L003b: cmp dword [0x621e5188], 0x0
L0042: jz L0049
L0044: call 0x62023890
L0049: xor eax, eax
L004b: mov [ebp-0x18], eax
L004e: mov byte [esi+0x8], 0x1
L0052: mov eax, [ebp-0x24]
L0055: mov [esi+0xc], eax
L0058: lea esp, [ebp-0xc]
L005b: pop ebx
L005c: pop esi
L005d: pop edi
L005e: pop ebp
L005f: ret
However I'm a bit out of my league when it comes to the assembly part.
Is there any intermediate there as seen as discussed earlier? I'm interested in if the
int num = a;
int num2 = b;
were completely optimized out or not. I'm also interested in why the compiler would create intermediates in the release version (is there a reason?) or if it's a decompilation artifact from SharpLab.
This is for a game engine where GC pauses are bad.
As the cost of a GC pause maters, it is abundantly clear: You are doing realtime programming . And I can only say that Realtime Programming and GC Memory Management do not mix.
You might be able to fix this problem, but there is going to be another one. And then another one after that. And then more and more, until you finally realize you were on a dead end. The sooner you realize that you are likely on a dead end, the more work you will be able to salvage.
Historically game engines - especially the drawing code - are unmanaged code that uses direct memory management. .NET Code is bitness agnostic. But once you used professional drawing code, you were basically tied down to its bitness. However I can not tell if that was just inertia (we used that engine before and will not change it, just because the language/runtime did) or if it was important for performance.
I also can not say how much of Unity drawing code uses unmanaged code. But as Unity games need to be built for specific platforms, I am going to assume: More than none. So it might well be that not going into unmanaged code is impossible when doing game engines.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.