堆与堆栈分配的含义(.NET)
Posted
技术标签:
【中文标题】堆与堆栈分配的含义(.NET)【英文标题】:Heap versus Stack allocation implications (.NET) 【发布时间】:2010-10-03 09:20:23 【问题描述】:来自SO answer1关于堆和堆栈的问题,它向我提出了一个问题:为什么知道变量的分配位置很重要?
another answer 有人指出堆栈更快。这是唯一的暗示吗?有人可以给出一个简单的分配位置更改可以解决问题(例如性能)的代码示例吗?
请注意,这个问题是特定于 .NET 的
1 问题已从 SO 中删除。
【问题讨论】:
未来的 Google 员工请注意,此 c# 问题中的第一个链接已损坏(如图所示),第二个链接 是关于stack allocation in general
并且不特定于 .NET。
正如 Warren P 的 66 票有用的投票所指出的(在撰写本文时):"Is it snarky to point out that it is the success of modern "managed code" languages that awareness of stack and heap are no longer necessary (much) for C# and Java developers, because those of us who have to debug things that we did wrong in C and C++ needed to understand the model so we can fix our code when it breaks. In other words, 'ignorance is power', because the abstraction works as designed, allowing me to work on a virtual machine that is easier to use than the real one."
当前答案中从未提及:在堆栈上分配的值可能必须在堆上重新定位,然后多次返回。这就是装箱和拆箱的作用,例如当使用非通用集合(object
存储在堆中的集合)时,当项目被强制转换为例如int
(存储在堆栈中),然后重新装箱(例如,因为它们在方法中作为 object
传递)。成本是巨大的。它可以通过设计不需要装箱/拆箱的变量和方法参数来减少。 More.
【参考方案1】:
只要您知道语义是什么,堆栈与堆的唯一后果就是确保您不会溢出堆栈,并意识到垃圾收集堆会产生相关成本。
例如,JIT 可以注意到新创建的对象从未在当前方法之外使用(引用永远不会在其他地方转义)并将其分配到堆栈上。目前还没有这样做,但这样做是合法的。
同样,C# 编译器可以决定在堆上分配所有局部变量 - 堆栈将只包含对 MyMethodLocalVariables 实例的引用,所有变量访问都将通过它实现。 (事实上,委托或迭代器块捕获的变量已经具有这种行为。)
【讨论】:
链接已损坏。有谁知道这篇文章还能在哪里阅读? @JamesHoux:恐怕我觉得我已经没有这个了。将删除该段。但它可能与blogs.msdn.microsoft.com/ericlippert/2009/04/27/… 非常相似【参考方案2】:(edit: 我的原始答案包含过于简单化的“结构是在堆栈上分配的”,并且有点混淆了堆栈与堆和值与引用的关系,因为它们是在 C# 中耦合。)
对象是否在堆栈上是一个实现细节,并不是很重要。乔恩已经很好地解释了这一点。在使用类和结构之间进行选择时,更重要的是要意识到引用类型与值类型的工作方式不同。以如下简单类为例:
public class Foo
public int X = 0;
现在考虑以下代码:
Foo foo = new Foo();
Foo foo2 = foo;
foo2.X = 1;
在这个例子中, foo 和 foo2 是对同一个对象的引用。在 foo2 上设置 X 也会影响 foo1。 如果我们将 Foo 类更改为结构,则不再是这种情况。这是因为结构不是通过引用访问的。分配 foo2 实际上会创建一个副本。
将东西放入堆栈的原因之一是垃圾收集器不必清理它。您通常不应该担心这些事情;只需使用课程!现代垃圾收集器做得很好。一些现代虚拟机(如 java 1.6)甚至可以determine whether it is safe to allocate objects on the stack,即使它们不是值类型。
【讨论】:
虽然你的例子和解释是正确的,但你已经为你的第一句话展示了一个反例。什么是“Foo.X”?一个 int,它是一种值类型 - 但它始终存在于堆上。 “类是堆分配的,结构是堆栈分配的”的说法过于简单了。 更好的措辞是说值类型在它们被声明的地方分配。如果它们在函数中被声明为局部变量,它们就在堆栈上。如果它们被声明为类中的成员,它们就在堆上,作为该类的一部分。 我不会投票赞成这个答案,因为它混合了引用与值的含义以及堆与堆栈的含义。 感谢更正,它触发了“啊哈!”片刻。我现在意识到 stack-vs-heap 在 C# 中是一个相对不重要的实现细节,而 value-vs-reference 是这里真正的问题。我应该这样解释的。 有人可能会纠正我,但是类成员/属性不是也存储在堆上吗?例如人.年龄 = 30;在堆上【参考方案3】:在 .NET 中几乎没有什么可讨论的,因为决定在哪里分配实例的不是类型的用户。
引用类型总是在堆上分配。值类型默认分配在堆栈上。例外情况是,如果值类型是引用类型的一部分,在这种情况下,它会与引用类型一起在堆上分配。 IE。类型的设计者代表用户做出此决定。
在 C 或 C++ 等语言中,用户可以决定数据的分配位置,在某些特殊情况下,与从堆中分配相比,从堆栈分配可能要快得多。
这与 C / C++ 如何处理堆分配有关。事实上,.NET 中的堆分配非常快(除非它触发垃圾收集),所以即使您可以决定在哪里分配,我的猜测是差异不会很大。
但是,由于堆是垃圾收集而堆栈不是,显然在某些情况下您会看到一些差异,但考虑到您在 .NET 中实际上没有选择的事实,这几乎不相关。
【讨论】:
感谢这个基本评论:在 .NET 中几乎没有什么可讨论的,因为决定在哪里分配实例的不是类型的用户,而在 C 或 C++ 等语言中,用户可以决定数据的分配位置,对于某些特殊情况,从堆栈分配可能比从堆分配快得多。【参考方案4】:我认为最简单的原因是,如果它在堆中,那么一旦不再需要该变量,垃圾收集就需要处理它。在堆栈上时,该变量会被任何正在使用它的东西解除,例如实例化它的方法。
【讨论】:
【参考方案5】:在我看来,当您真正开始考虑应用程序的性能时,了解堆栈和堆之间的差异以及如何在其上分配事物会非常有帮助。 以下问题使理解这些差异变得至关重要: 您认为 .NET 访问什么速度更快、效率更高? - 堆栈或堆。 在什么情况下.NET 可以放置堆的值类型?
【讨论】:
【参考方案6】:与流行的看法相反,.NET 进程中的堆栈和堆之间没有太大区别。堆栈和堆只不过是虚拟内存中的地址范围,与为托管堆保留的地址范围相比,为特定线程的堆栈保留的地址范围没有固有的优势。 访问堆上的内存位置既不比访问堆栈上的内存位置快也不慢。在某些情况下,有几个考虑因素可能支持对堆栈位置的内存访问速度更快的说法,总体而言,比对堆位置的内存访问。其中:
-
在堆栈上,时间分配局部性(在时间上紧密地进行分配)意味着空间局部性(在空间上紧密地存储在一起)。反过来,当时间分配局部性意味着时间访问局部性(一起分配的对象被一起访问)时,顺序堆栈存储在 CPU 缓存和操作系统分页系统方面往往表现更好。
由于引用类型开销,堆栈上的内存密度往往高于堆上的内存密度。更高的内存密度通常会带来更好的性能,例如,因为更多的对象适合 CPU 缓存。
线程堆栈往往相当小——Windows 上默认的最大堆栈大小为 1MB,大多数线程实际上往往只使用几个堆栈页。在现代系统上,所有应用程序线程的堆栈都可以放入 CPU 缓存中,这使得典型的堆栈对象访问速度非常快。 (另一方面,整个堆很少适合 CPU 缓存。)
话虽如此,您不应该将所有分配转移到 堆! Windows上的线程栈有限,容易耗尽 通过应用不明智的递归和大堆栈来堆栈 分配。
【讨论】:
这很有道理,非常感谢您抽出宝贵时间提供如此详细的回复! 这个答案的一部分是不完整的。答案是“...Windows 上的默认最大堆栈大小为 1MB”。这仅适用于 32 位进程。 64 位 Windows 进程的默认堆栈大小为 4MB。【参考方案7】:我在堆栈和堆上使用不同的基准测试了很多,我总结如下:
的类似性能 小应用程序(堆上没有太多对象) 对象大小
提高堆栈性能(快 1 倍 - 5 倍)
大小为 1Kb 到 100KB 的对象性能更佳(速度提高 100 倍甚至更多)
大量对象 大内存压力 - 每秒分配大量内存和满内存 大型对象 10KB - 3MB(我想是 x64 系统) XBox(慢速垃圾收集器)最好的方法是数组池。它和堆栈一样快,但你没有堆栈那样的限制。
使用堆栈的另一个含义是通过设计它是线程安全的。
x64 Windows 上堆栈的默认内存限制为 4MB。因此,分配不超过 3MB 的空间是安全的。
【讨论】:
这里有一篇关于数组池使用的优秀文章(如答案中所建议):adamsitnik.com/Array-Pool 为了澄清这个答案的其他读者:显示的性能差异不是由于“堆栈与堆”本身。较大对象的成本是由于垃圾收集器将 gen0 提升为 gen1 和 gen2 对象。在小对象的情况下,这些对象可能都保留为 gen0,这使得它们的性能与堆栈相当。作者 Tomas Kubes 可能意识到了这一点……正如他所建议的使用数组池,它完全绕过了 GC 成本并使堆性能与堆栈相同。 另外,对于在处理非常大的对象堆时对数组池感兴趣的任何人,看看桩。堆是一种在单个巨大数组中表示对象的方法,使垃圾收集器完全不可见它们并提供令人难以置信的性能。以上是关于堆与堆栈分配的含义(.NET)的主要内容,如果未能解决你的问题,请参考以下文章