【问题标题】:Heap versus Stack allocation implications (.NET)堆与堆栈分配的含义(.NET)
【发布时间】:2010-10-03 09:20:23
【问题描述】:

来自SO answer1关于堆和堆栈的问题,它向我提出了一个问题:为什么知道变量的分配位置很重要?

another answer 有人指出堆栈更快。这是唯一的暗示吗?有人可以给出一个简单的分配位置更改可以解决问题(例如性能)的代码示例吗?

请注意,这个问题是特定于 .NET 的

1 问题已从 SO 中删除。

【问题讨论】:

标签: c# .net performance stack heap-memory


【解决方案1】:

只要您知道语义是什么,堆栈与堆的唯一后果就是确保您不会溢出堆栈,并意识到垃圾收集堆会产生相关成本。

例如,JIT 可以注意到新创建的对象从未在当前方法之外使用(引用永远不会在其他地方转义)并将其分配到堆栈上。目前还没有这样做,但这样做是合法的。

同样,C# 编译器可以决定在堆上分配所有局部变量 - 堆栈将只包含对 MyMethodLocalVariables 实例的引用,所有变量访问都将通过它实现。 (事实上​​,委托或迭代器块捕获的变量已经具有这种行为。)

【讨论】:

【解决方案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 进程中的堆栈和堆之间没有太大区别。堆栈和堆只不过是虚拟内存中的地址范围,与为托管堆保留的地址范围相比,为特定线程的堆栈保留的地址范围没有固有的优势。 访问堆上的内存位置既不比访问堆栈上的内存位置快也不慢。在某些情况下,有几个考虑因素可能支持对堆栈位置的内存访问速度更快的说法,总体而言,比对堆位置的内存访问。其中:

      1. 在堆栈上,时间分配局部性(在时间上紧密地进行分配)意味着空间局部性(在空间上紧密地存储在一起)。反过来,当时间分配局部性意味着时间访问局部性(一起分配的对象被一起访问)时,顺序堆栈存储在 CPU 缓存和操作系统分页系统方面往往表现更好。
      2. 由于引用类型开销,堆栈上的内存密度往往高于堆上的内存密度。更高的内存密度通常会带来更好的性能,例如,因为更多的对象适合 CPU 缓存。
      3. 线程堆栈往往相当小——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 成本并使堆性能与堆栈相同。
      • 另外,对于在处理非常大的对象堆时对数组池感兴趣的任何人,看看桩。堆是一种在单个巨大数组中表示对象的方法,使垃圾收集器完全不可见它们并提供令人难以置信的性能。
      猜你喜欢
      • 2011-10-06
      • 1970-01-01
      • 2011-05-04
      • 2011-05-28
      • 2012-12-05
      • 2011-09-06
      • 2013-10-20
      • 2023-03-03
      相关资源
      最近更新 更多