简体   繁体   中英

C#: Does Deconstruct(…) generate extra junk assignments in the compiled output?

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM