C# returns 如何构建

How C# returns Structs

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

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

但我很好奇 C# 是如何处理结构的 return 的。

在 C 中,return 由寄存器生成,或者如果要 returned 的值对于寄存器来说太大,则使用堆引用。实际上所有 C# 结构教程都说结构存在于堆栈中,从不存在于堆中。

所以在下面的代码中:

MyStruct ms = GetMyValue();

其中GetMyValue()

MyStruct GetMyValue();

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


编辑:

解决 post 中留下的评论:

  1. 我在 post 之前阅读了一些关于 C# 结构的教程,this tutorial in particular uses the word stack more times than I bother to count. And this MSDN tutorial 也谈到了堆栈,虽然它是从 2003 年开始的,但我不认为结构从那时起改变了。

  2. 我知道 C# 可能根本不会实现这一点,但实际上是 JIT 编译器本身或 CLR 或其他我不知道的问题。这就是我的问题的目的,了解更多关于 C# 的内部工作原理,即使这实际上与语言本身无关。

  3. 有C函数调用约定,最支持我的Post是this Whosebug post。当我第一次 post 在这里编辑它时,我只是说了我记得的,但是因为 SO 回答说:

    As for your specific question, it depends the ABI. Sometimes if the return value is larger than 4 bytes but not larger than 8 bytes, it can be split into EAX and EDX. But most of the time the calling function will just allocate some memory (usually on the stack) and pass a pointer to this area to the called function.

    我在这个问题上可能是错误的,我说 可能,因为答案是 通常

  4. 我想了解如何处理结构的真正原因是因为我有一个项目,我必须多次读取串行端口以轮询数据,该数据将是 return 通过一种方法编辑。

    因为数据只是一些字节,我想我可以从结构中获得一些性能,而不是使用 class 来抽象串行端口传入的字节,但是如果 return 会通过 struct 作为堆分配我对性能提升的期望可能是错误的。

    是的,我可以做一个简单的测试并比较性能,我知道,但我想实际学习它是如何完成的窗帘,不仅要记住我模拟的结果。我想知道我使用的东西实际上是如何工作的,而不仅仅是学习如何使用它们。

值类型不仅位于堆栈上。他们也生活在领域和阵列中。与引用类型的主要区别在于,值类型是按值复制的,没有标识。堆栈与堆的想法是错误的。

In C the return is made by registers, or by reference using the heap if the value to be returned is too big for the registers

不涉及堆。调用者为要放置的 return 值分配 spaces。它传递一个指向 space 的指针。被调用者可以填写space。 .NET CLR 也这样做。当然这是一个实现细节。

but I wanted to actually learn

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

I can see how structs can degrade the performance of methods when structs are passed as parameters, since they will be always copied in the stack

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

结构并不总是存在于堆栈中。如果你在函数内部分配一个结构,它的生命就存在于堆栈中。如果它是一个引用类型的字段(class/array(隐式派生自 System.Array/Object),它就在堆上生活。至于它们如何被 returned,那可能是该 CPU 架构的 ABI。

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

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,它向下分配和调整 ESP(扩展堆栈指针) sizeof(MyStruct) 的数量,发出 OpCodes.Stloc 以将 ESP 减去 sizeof(MyStruct) 存储到保留的本地索引中,用它的字段做一些事情,调用 Emit(OpCodes.Ldloc, 0) 来推送地址本地指向调用函数的评估堆栈,并发出 OpCodes.Ret 到 return.

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

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

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