【问题标题】:How C# returns StructsC# 如何返回结构
【发布时间】:2015-10-23 16:54:04
【问题描述】:

结构是值类型,因此每次对结构进行操作时都会完全复制。由于它们是值类型,因此结构被分配在堆栈中而不是堆中。

当结构作为参数传递时,我可以看到结构如何降低方法的性能,因为它们总是会被复制到堆栈中,特别是当它们很大并且有很多内部字段时。

但我很好奇 C# 如何处理结构的返回。

在 C 中,返回是通过寄存器进行的,或者如果要返回的值对于寄存器来说太大,则使用堆进行引用。几乎所有的 C# 结构教程都说结构存在于堆栈中,而不是堆中。

所以在下面的代码中:

MyStruct ms = GetMyValue();

GetMyValue() 在哪里

MyStruct GetMyValue();

C# 将如何处理 ms 变量的结构返回?特别是如果它对于寄存器来说太大了?它实际上是否会将其复制到堆中,然后再次将其复制回方法的调用者并将其分配给 ms?


编辑:

解决帖子中留下的cmets:

  1. 在发布这篇文章之前,我已经阅读了一些关于 C# 结构的教程,this tutorial 特别是使用了 stack 这个词的次数比我数的还要多。而this MSDN tutorial 也谈到了堆栈,虽然它是从 2003 年开始的,但我认为从那时起结构没有改变。

  2. 我知道这可能与 C# 完全无关,但实际上是 JIT 编译器本身或 CLR 或其他我不知道的问题。这就是我提出问题的目的,以便更多地了解 C# 的内部工作原理,即使这实际上与语言本身无关。

  3. 有C函数调用约定,对我的帖子最好的支持是this StackOverflow post。当我第一次在这里发布它时,我只是说出了我记得的内容,但是由于 SO 答案说:

    至于您的具体问题,这取决于 ABI。有时如果返回值大于 4 字节但不大于 8 字节,可以拆分为 EAX 和 EDX。但大多数时候调用函数只会分配一些内存(通常在堆栈上),并将指向该区域的指针传递给被调用函数。

    我可能在这个问题上错了,我说可能,因为答案说通常。

  4. 我想了解如何处理结构的真正原因是因为我有一个项目,我必须多次读取串行端口来轮询数据,这些数据将由方法返回。

    由于数据只是一些字节,我认为我可以从结构中获得一些性能,而不是使用类来抽象串行端口传入的字节,但如果 return 将 struct 作为堆传递我对性能提升的期望可能是错误的。

    是的,我可以做一个简单的测试并比较性能,我知道,但我想真正了解它是如何在幕后完成的,而不仅仅是记住我的模拟结果。我想知道我所使用的东西是如何工作的,而不仅仅是学习如何使用它们。

【问题讨论】:

  • And practically all C# struct tutorials say structs lives in the stack, never in the heap. 他们不都这么说。任何这样做都是完全错误的。 In C the return is made by registers, or by reference using the heap 不,堆永远不会用于将返回值传递给调用者。
  • 它可能会便宜一点,但总是复制一个值类型以获得回报。要有效地使用结构,您需要使用 ref/out 参数。提示:使用类。如果不确定,请对结构进行基准测试。几乎不花时间。
  • 我不明白投反对票的意义。当有人试图学习一些你能做的一切时?即使他在很多地方都错了,你也不应该投反对票。我不是说我知道这一点。我也在学习,但最好说出拒绝投票的原因。
  • 不是一个完全的骗局,但这个related question 的答案会让你明白。
  • 这个问题中几乎每个句子都是假的。以如此多的谎言为基础的问题很难回答。

标签: c# class struct clr return-value


【解决方案1】:

值类型不仅位于堆栈上。它们也存在于字段和数组中。引用类型的主要区别在于值类型是按值复制的,并且没有标识。堆栈与堆的想法是错误的。

在 C 中,返回是通过寄存器进行的,或者如果要返回的值对于寄存器来说太大,则使用堆进行引用

不涉及堆。调用者为要放置的返回值分配空间。它传递一个指向该空间的指针。被调用者可以填充该空间。 .NET CLR 也可以做到这一点。当然这是一个实现细节。

但我想真正学习

这很好。你不可能测试我刚才告诉你的。你需要对你相信别人所说的话更加挑剔。要么你有糟糕的教程,要么你以不精确的方式阅读它们。

当结构作为参数传递时,我可以看到结构如何降低方法的性能,因为它们总是会被复制到堆栈中

我认为并非总是如此。我不太确定,但我认为 JIT 有时可以在寄存器中传递结构。 .NET JIT 确实没有进行太多优化,但我认为这是一种在一定程度上起作用的优化。可能是由一些单字段结构的存在驱动的,例如DateTime。

【讨论】:

  • 是的,根据 cmets 中发布的链接,JIT 似乎可以根据结构大小优化回报。感谢您的澄清。
【解决方案2】:

结构并不总是存在于堆栈中。如果你在函数内部分配一个结构,它的生命就在堆栈上。如果它是引用类型的字段(类/数组(从 System.Array/Object 隐式派生),它的生命就在堆上。至于它们的返回方式,这可能取决于该 CPU 架构的 ABI。

从听起来,你从来没有处理过 IL/汇编/代码生成,所以让我们构建一个动态方法,它等效于 MyStruct ms = GetMyValue()/编译器在单词堆栈上下文中生成的内容。 “事物”从未真正返回。事物(我确定在元组意义上)被推入堆栈,然后发出返回指令。为调用者留下返回值。我们将假设 GetMyValue() 分配一个新的 MyStruct 并将其分配给一个局部变量。生成的代码看起来像这样(我扩展了 ILGenerator 类):

ILGenerator generator = dynamicMethod.GetILGenerator();

generator
    .DeclareLocal(typeof(MyStruct))
    .EmitCall(OpCodes.Call, typeof(EncapsulatingClass).GetMethod("GetMyValue"))
    .Emit(OpCodes.Stloc, 0);

这里发生的事情是(其中一些是我对 CLI 运行时如何工作的假设):

  1. 调用函数在当前本地列表索引处保留一个 typeof(MyStruct) 的槽。

  2. GetMyValue() 被调用,以与我们正在构建的方法相同的方式保留一个 MyStruct 本地,发出一个 OpCodes.Newobj,它在 sizeof(MyStruct) 的数量中向下分配和调整 ESP(扩展堆栈指针),发出OpCodes.Stloc 将 ESP 减去 sizeof(MyStruct) 存储到保留的本地索引中,对其字段进行一些处理,调用 Emit(OpCodes.Ldloc, 0) 将本地指向的地址推送到调用函数的评估堆栈上,并发出一个 OpCodes.Ret 返回。

  3. 调用函数发出一个 OpCodes.Stloc 来存储(复制)评估堆栈顶部指向的 MyStruct 的内容(这是如何发生的,我确信答案取决于它,不幸的是),在本地索引 0。

我不是 CLI 运行时是如何构建的专家,所以很多这都是对所发生情况的假设。持保留态度,我绝不是 CPU 工程专家。 OpCodes.Ldloc、OpCodes.Ret、OpCodes.Stloc -- ms = GetMyValue() -- 的指令流段如何处理,可能取决于 JITer 如何将 IL 转换为实际的 cpu 特定机器指令。比如X86。决定一个结构是否将返回到寄存器的因素可能仅限于一个寄存器,因此无论最大的寄存器是什么,以及是否适合其中的任何结构。我知道 CPU 可以组合寄存器来获得内存偏移量,但我不确定这是否适用于返回多个寄存器内的结构。要记住的另一件事,GetMyValue() 超出范围,这意味着分配的结构 GetMyValue() 在范围意义上不再存在,但在堆栈意义上(分配的位置),它确实存在,因此 JITer 很可能只是将地址 OpCodes.Ldloc 压入堆栈,并将其直接放入调用者本地索引 0 中。因为由于函数返回,任何东西都无法再复制它。使调用者成为结构的新所有者。在这种特殊情况下,完全避免任何复制和注册。这可能也是调用约定发挥作用的地方。问题是,如果您出于某种原因在 GetMyValue() 中分配了三个结构,则在分配的第一个结构之后返回任何结构都会破坏该优化,这是下一次优化的地方,将结构返回到寄存器中(如果适合的话)玩。离开最坏的情况,为调用者再次将其内容纯粹复制到堆栈中。我可能是错的,非常欢迎任何人加入并纠正我。一个好的起点是 github,看看运行时如何处理结构的 OpCodes.Ldloc/Stloc。我想这是获得所需答案的好地方。

编辑:你读过的任何教程都说结构总是分配在堆栈上,让它们都被 DDoS 攻击。

【讨论】:

  • “如果你在函数内部分配一个结构,它的生命就在堆栈上”——如果它是一个异步方法,或者一个迭代器块等;即使这是一个实际上错误的过度简化
  • 如果它是在异步方法中分配的,那么这是“引用类型内部”的一部分。我认为大多数人都了解 TPL 内部/任务队列。我想如果他不这样做是不对的。
  • 甚至比这更复杂;至少在发布版本中,构造的状态机是一种值类型。它是否最终在堆上取决于它是否曾经遇到不完整的延续。真的很微妙。
  • 我们还可以涵盖其他边缘情况,例如“捕获的变量”——这么多“例外”!
猜你喜欢
  • 2021-09-09
  • 1970-01-01
  • 2014-04-22
  • 2020-03-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-13
相关资源
最近更新 更多