【问题标题】:Is there a way to inline tuple deconstruction to avoid an unnecessary allocation?有没有办法内联元组解构以避免不必要的分配?
【发布时间】:2025-12-06 23:25:01
【问题描述】:

我有以下例子struct:

struct Data {
    internal long a;
    internal long b;

    internal void Deconstruct(out long aa, out long bb) {
        aa = a; bb = b;
    }
}

如果我只想使用结构的值而忘记结构本身怎么办?

Data Generate()
    => new Data() { a = 3, b = 5 };

void Test() {
    (var a, var b) = Generate();
    Console.WriteLine(a);
    Console.WriteLine(b);
}

Generate 的调用会创建一个结构,并立即将其分解为各个部分。我可以以某种方式内联这个过程并完全摆脱结构吗?

我在发布模式下使用 VS 15.5.7 编译(此类库),显示 ilspy:

.method private hidebysig 
    instance void Test () cil managed 
{
    // Method begins at RVA 0x208c
    // Code size 33 (0x21)
    .maxstack 3
    .locals init (
        [0] int64,
        [1] valuetype *Demo.Q1/Data,
        [2] int64,
        [3] int64
    )

    IL_0000: ldarg.0
    IL_0001: call instance valuetype *Demo.Q1/Data *Demo.Q1::Generate()
    IL_0006: stloc.1
    IL_0007: ldloca.s 1
    IL_0009: ldloca.s 2
    IL_000b: ldloca.s 3
    IL_000d: call instance void *Demo.Q1/Data::Deconstruct(int64&, int64&)
    IL_0012: ldloc.2
    IL_0013: ldloc.3
    IL_0014: stloc.0
    IL_0015: call void [System.Console]System.Console::WriteLine(int64)
    IL_001a: ldloc.0
    IL_001b: call void [System.Console]System.Console::WriteLine(int64)
    IL_0020: ret
} // end of method Q1::Test

【问题讨论】:

  • 好吧,你为什么不首先使用ValueTuple?为什么要定义struct
  • 为什么要内联呢? JIT 编译器处理内联决策,因为它具有与内联是否是有效优化最相关的信息。您真的应该查看 JIT 的机器代码。
  • @mikez 这很重要。我想:内联它并优化分配 - 这可能是不可能的。
  • 还有哪个分配会被删除?结构体不会给 GC 增加内存压力,因此与类相比,结构体的开销较低。
  • @CamiloTerevinto 我试过了,确实有区别。使用ValueTuple,值直接从评估堆栈中获取,不需要ValueTuple 的局部变量。

标签: c# struct tuples inline c#-7.0


【解决方案1】:

如果你想忘记结构,而不是

Data Generate()
    => new Data() { a = 3, b = 5 };

为什么不使用

void Generate(out long aa, out long bb)
{
    aa = 3; bb = 5;
}

这消除了你所要求的结构。

如果你想保留结构但摆脱额外的分配,你也可以这样做:

class Data {
    public long A { get; internal set; }
    public long B { get; internal set; }

    internal Data(long a, long b) {
        A = a; B = b;
    }
}

然后使用:

void Test() {
    var data = Generate();
    Console.WriteLine(data.A);
    Console.WriteLine(data.B);
}

【讨论】:

  • 虽然这个解决方案有效,但我想保留我的结构。我想在其中定义属性或其他元素。如果我的结构包含更多字段怎么办?
  • 你觉得我回答的后半部分怎么样?您仍然可以返回单个数据结构,但不需要解构参数。您可以将它们作为属性访问。您可以添加任意数量的方法,包括接口。
  • @ventiseis 我真的不确定你的问题。如果您不想构造它,为什么要故意使用结构?为什么void Generate(..) 的建议在这里没有帮助?
  • @Rob 恐怕我把这个例子简单化了。 Generate 方法确实有输入参数,并且结构已经计算了只读属性——我很乐意使用它并将它作为参数或返回值传递。但是在这种情况下,我根本不需要结构,只需要计算中的两个值。因此,如果我不想复制计算代码,我将不得不使用这个答案中提出的结构。而且我不会使用元组分解,因为它增加了另一层间接性。
【解决方案2】:

根据 mikez 的建议,我使用 windbgsos 扩展名来查看 jitted 代码。

我从中学到了三件事:

  • jitting后,这种情况下栈上没有分配struct
  • 查看生成的 il 代码不足以推断运行时执行的代码
  • 这个简化的示例可能与我的原始用例不同 - 所以我必须查看原始示例的 jitted 代码

感谢您提供的所有有用意见。

无需进一步修改原始源代码,这是!Name2EETest 方法的输出:

C:\Users\...\source\repos\*Demo\Q1.cs @ 37:
>>> 00007ffa`02c41d00 56              push    rsi
00007ffa`02c41d01 4883ec30        sub     rsp,30h
00007ffa`02c41d05 33c0            xor     eax,eax
00007ffa`02c41d07 4889442420      mov     qword ptr [rsp+20h],rax
00007ffa`02c41d0c 4889442428      mov     qword ptr [rsp+28h],rax
00007ffa`02c41d11 488d542420      lea     rdx,[rsp+20h]
00007ffa`02c41d16 e8dde6ffff      call    00007ffa`02c403f8 (*Demo.Q1.Generate(), mdToken: 0000000006000003)
00007ffa`02c41d1b 488b4c2420      mov     rcx,qword ptr [rsp+20h]
00007ffa`02c41d20 488b742428      mov     rsi,qword ptr [rsp+28h]

C:\Users\...\source\repos\*Demo\Q1.cs @ 38:
00007ffa`02c41d25 e8eee5ffff      call    00007ffa`02c40318 (System.Console.WriteLine(Int64), mdToken: 0000000006000080)

C:\Users\...\source\repos\*Demo\Q1.cs @ 39:
00007ffa`02c41d2a 488bce          mov     rcx,rsi
00007ffa`02c41d2d e8e6e5ffff      call    00007ffa`02c40318 (System.Console.WriteLine(Int64), mdToken: 0000000006000080)

C:\Users\...\source\repos\*Demo\Q1.cs @ 40:
00007ffa`02c41d32 90              nop
00007ffa`02c41d33 4883c430        add     rsp,30h
00007ffa`02c41d37 5e              pop     rsi
00007ffa`02c41d38 c3              ret

对于Generate 方法:

C:\Users\...\source\repos\*Demo\Q1.cs @ 28:
>>> 00007ffa`02c41d50 b803000000      mov     eax,3
00007ffa`02c41d55 b905000000      mov     ecx,5
00007ffa`02c41d5a 488902          mov     qword ptr [rdx],rax
00007ffa`02c41d5d 48894a08        mov     qword ptr [rdx+8],rcx
00007ffa`02c41d61 488bc2          mov     rax,rdx
00007ffa`02c41d64 c3              ret

据我了解,堆栈上没有分配结构。

【讨论】:

    最近更新 更多