【问题标题】:C#: Does Deconstruct(...) generate extra junk assignments in the compiled output?C#: Deconstruct(...) 是否在编译输出中生成额外的垃圾分配?
【发布时间】:2020-05-06 20:46:12
【问题描述】:

我正在检查解构是否会导致在堆上实例化额外的对象,因为我在需要尽可能少的 GC 压力的区域中做某事。这是我正在尝试的代码:

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);
    }
}

我通过SharpLab 运行了这个,看看 C# 为我做了什么,它做了以下事情:

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());
}

这回答了我最初是否需要担心额外分配的问题......但更有趣的是,发布模式(因为上面是调试)有:

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());
}

但是这可以减少到(这是我做一些额外的变量修剪numnum2):

public static void Main()
{
    int a;
    int b;
    new Pair(1, 2).Deconstruct(out a, out b);
    Console.WriteLine(a.ToString() + " " + b.ToString());
}

这是一个有趣的问题,因为两个整数的额外堆栈分配对我的程序性能没有任何意义。为了好玩,虽然我尝试查看Main 的 IL 并得到:

// 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

JIT ASM 是

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

但是,在组装部分方面,我有点不合群。

如前所述,那里有任何中间体吗?我感兴趣的是

int num = a;
int num2 = b;

是否已完全优化。我还对为什么编译器会在发布版本中创建中间体(有原因吗?)或者它是否是 SharpLab 的反编译工件感兴趣。

【问题讨论】:

  • “我正在检查解构是否会导致额外的对象在堆上被实例化,因为我在一个需要尽可能少的 GC 压力的区域做某事”这听起来像一个 XY问题。你为什么有这个要求?与其尝试改变 GC 的作用,不如完全避开它,使用非托管内存?
  • @Christopher 这适用于 GC 暂停很糟糕的游戏引擎。除非我别无选择,否则我绝对喜欢做非托管内存,尤其是当一个简单、更易读、更安全的替代方案可用时,除非我已经用尽所有选项。话虽如此,我宁愿将此线程专门针对手头的问题,而不是问题的前奏。
  • 我理解您不愿意使用裸指针太好了。我学习了原生 C++ 编程。与此相比,.NET 简直是美梦成真。我永远不会选择 回到“处理裸指针”。对我来说,这就像玩手榴弹一样令人向往。但这可能是真的没有其他选择的情况。
  • 即使你不能使用它,也可以看看 Unitys HPC (High Performance C#)。关于这个概念及其推理有一些很好的读物。

标签: c# cil


【解决方案1】:

这适用于 GC 暂停很糟糕的游戏引擎。

由于 GC 暂停的成本很重要,很明显:您正在执行实时编程。而且我只能说实时编程和 GC 内存管理不能混用。

您也许可以解决这个问题,但还会有另一个问题。然后是另一个。然后越来越多,直到你终于意识到自己走上了死胡同。越早意识到自己可能陷入死胡同,就能挽救的工作就越多。

从历史上看,游戏引擎(尤其是绘图代码)是使用直接内存管理的非托管代码。 .NET 代码与位数无关。但是一旦你使用了专业的绘图代码,你基本上就被它的位数所束缚。但是我无法判断这是否只是惯性(我们之前使用过该引擎并且不会更改它,只是因为语言/运行时做了)还是它对性能很重要。

我也不能说有多少 Unity 绘图代码使用了非托管代码。但是由于需要为特定平台构建 Unity 游戏,我将假设:多于没有。所以很可能在做游戏引擎时不进入非托管代码是不可能的。

【讨论】:

    最近更新 更多