为什么堆栈内存大小如此有限?


81

在堆上分配内存时,唯一的限制是可用RAM(或虚拟内存)。它使Gb的内存。

那么,为什么堆栈大小如此有限(大约1 Mb)呢?什么技术原因阻止您在堆栈上创建非常大的对象?

更新:我的意图可能不清楚,我不想在堆栈上分配大对象,也不需要更大的堆栈。这个问题仅仅是出于好奇。


为什么在堆上创建大对象是可行的?(呼叫链通常会进入堆栈。)
Makoto

4
我认为真正的答案要比大多数答案所描绘的要简单:“因为这是我们一直以来所做的方式,到目前为止还可以,为什么要进行更改?”
杰里·科芬

@JerryCoffin您是否阅读了到目前为止发布的所有答案?对这个问题有更多的见识。
user1202136 2012年

3
@ user1202136:我已经阅读了所有内容,但是人们在猜测,我的猜测是,他们所引用的许多因素甚至在做出关于该主题的原始决定时都没有考虑。用一句话来表达,“有时雪茄只是雪茄。”
杰里·科芬

3
“我们应该使默认堆栈多大?” “哦,我不知道,我们可以运行多少个线程?” “它在K上的某个地方爆炸”“好,那么,我们将其称为2K,我们有2 Gig虚拟,那么大约1兆?“是的,好的,下一个问题是什么?”
马丁·詹姆斯

Answers:


45

我的直觉如下。堆栈不如堆容易管理。堆栈需要存储在连续的内存位置。这意味着您不能根据需要随机分配堆栈,但为此至少需要保留虚拟地址。保留的虚拟地址空间的大小越大,可以创建的线程就越少。

例如,一个32位应用程序通常具有2GB的虚拟地址空间。这意味着,如果堆栈大小为2MB(pthreads中的默认值),则最多可以创建1024个线程。对于Web服务器等应用程序来说,这可能很小。将堆栈大小增加到100MB(即,您保留100MB,但不一定立即将100MB分配给堆栈),将线程数限制为大约20,即使对于简单的GUI应用程序,这也可能会受到限制。

一个有趣的问题是,为什么我们在64位平台上仍然有此限制。我不知道答案,但我认为人们已经习惯了一些“堆栈最佳实践”:请小心在堆上分配大对象,并在需要时手动增加堆栈大小。因此,没有人发现在64位平台上添加“巨大”堆栈支持很有用。


许多64位计算机只有48位地址(虽然比32位有较大的增益,但仍然有限)。即使有额外的空间,您也必须担心对页表的保留方式-也就是说,拥有更多空间总是会产生开销。分配一个新的段(mmap)而不是为每个线程保留巨大的堆栈空间可能很便宜,甚至更便宜。
edA-qa mort-ora-y 2012年

4
@ EDA-qamort-ORA-Y:这个答案是不谈论分配,它在谈论虚拟内存reserveration,这几乎是免费的,而且肯定是很多比MMAP更快。
Mooing Duck

33

尚未有人提及的一个方面:

有限的堆栈大小是一种错误检测和遏制机制。

通常,C和C ++中堆栈的主要工作是跟踪调用堆栈和局部变量,如果堆栈超出范围,则几乎总是在设计和/或应用程序行为中出错。

如果允许堆栈任意增大,这些错误(例如无限递归)将在很晚才被捕获,只有在操作系统资源耗尽之后。通过对堆栈大小设置任意限制可以防止这种情况。实际大小并不重要,除了要足够小以防止系统降级。


您可能对分配的对象有类似的问题(因为替换递归的某种方法是手动处理堆栈)。该限制迫使使用其他方式(不一定更安全/更简单/ ..)(请注意有关(玩具)列表实现的注释次数,std::unique_ptr用于编写析构函数(而不依赖于智能指针))。
Jarod42

15

这只是默认大小。如果您需要更多,则可以得到更多-通常是通过告诉链接器分配额外的堆栈空间。

大堆栈的不利之处在于,如果创建多个线程,则每个线程将需要一个堆栈。如果所有堆栈都在分配多MB,但不使用它,则会浪费空间。

您必须为您的程序找到适当的平衡。


有些人,例如@BJovke,认为虚拟内存基本上是免费的。的确,您不需要物理内存来支持所有虚拟内存。您必须至少能够将地址分配给虚拟内存。

但是,在典型的32位PC上,虚拟内存的大小与物理内存的大小相同-因为任何地址(无论虚拟与否)都只有32位。

由于进程中的所有线程共享相同的地址空间,因此必须在它们之间进行分配。在操作系统发挥作用之后,应用程序仅剩下2-3 GB。这个大小对于极限两者的物理虚拟内存,因为那里只是没有任何其它地址。


最大的线程问题是您无法轻松地将堆栈对象发信号给其他线程。生产者线程必须同步地等待消费者线程释放对象,或者必须进行昂贵且产生争用的深层副本。
马丁·詹姆斯

2
@MartinJames:没有人说所有对象都应该在堆栈上,我们正在讨论为什么默认堆栈很小。
Mooing Duck

空间不会浪费,堆栈大小只是连续虚拟地址空间的保留。因此,如果将堆栈大小设置为100 MB,则实际使用的RAM量取决于线程中的堆栈消耗。
BJovke '17

1
@BJovke-但是虚拟地址空间仍然会用完。在32位进程中,此限制为几GB,因此仅保留20 * 100MB会给您带来麻烦。
Bo Persson

7

一方面,堆栈是连续的,所以如果分配12MB,则当要低于所创建的内容时必须删除12MB。而且,移动物体变得更加困难。这是一个真实的示例,可能会使事情更容易理解:

假设您要在一个房间里堆放箱子。哪个更容易管理:

  • 将任何重量的箱子互相叠放,但是当您需要在底部放东西时,则必须撤消整个堆垛。如果您要从堆中取出物品并交给其他人,则必须取下所有箱子并将箱子移到另一个人的堆上(仅限堆放)
  • 您将所有箱子(除了很小的箱子)放在一个特殊的区域,不要在其他区域上堆放东西,而记下放在纸上(指针)的位置,然后放在纸上堆。如果您需要将盒子交给其他人,您只需将纸堆中的纸条交给他们,或者只给他们纸的复印件,然后将原件放在堆中。(堆栈+堆)

这两个例子是粗略的概括,在类比中有些地方公然是错误的,但它非常接近,希望可以帮助您看到这两种情况的优点。


@MooingDuck是的,但是您正在程序的虚拟内存中工作,如果我进入一个子例程,将某些东西放在堆栈上,然后从该子例程返回,则需要先取消分配或移动我创建的对象,然后才能展开堆栈返回到我来自哪里。
斯科特·张伯伦

1
尽管我的评论是由于误解(我删除了),但我仍然不同意这个答案。从堆栈顶部删除12MB实际上是一个操作码。它基本上是免费的。编译器也可以并且确实欺骗“堆栈”规则,因此不,他们不必在展开前将对象复制/移动以返回它。因此,我认为您的评论也不正确。
Mooing Duck

好吧,通常,分配12MB占用堆栈上的一个操作码超过100个堆无关紧要-可能低于实际处理12MB缓冲区的噪声水平。如果编译器在发现返回一个荒谬的对象时想作弊(例如,通过在调用之前移动SP以使对象空间成为调用者堆栈的一部分),那么TBH很好,返回此类信息的开发人员对象(而不是指针/引用)在某种程度上受到编程的挑战。
马丁·詹姆斯

@MartinJames:C ++规范还说该函数通常可以将数据直接放入目标缓冲区中,而不使用临时缓冲区,因此,如果您小心一点,按值返回12MB缓冲区没有开销。
Mooing Duck 2012年

3

以近到远的顺序来考虑堆栈。寄存器距离CPU较近(快速),堆栈距离较远(但仍相对较近),堆距离较远(访问缓慢)。

堆栈位于进程的堆上,但是仍然可以使用,因为它被连续使用,所以它可能永远不会离开CPU缓存,这使其比普通堆访问要快。这是保持堆栈合理大小的原因。使其尽可能快地缓存。分配大堆栈对象(可能会在溢出时自动调整堆栈大小)违反了这一原则。

因此,这是一种很好的性能范例,而不仅仅是过去的遗留问题。


4
尽管我确实认为缓存在人为减小堆栈大小的原因中起着重要作用,但我必须在“堆栈位于堆中”这一说法中对您进行纠正。堆栈和堆都(虚拟地或物理地)存在于内存中。
塞巴斯蒂安·霍亚斯

“近或远”与访问速度有何关系?
MinhNghĩa19年

@MinhNghĩa好吧,RAM中的变量被缓存在L2存储器中,然后被缓存在L1存储器中,甚至那些也被缓存在寄存器中。对RAM的访问速度很慢,对L2的访问速度更快,L1的访问速度仍然更快,寄存器的访问速度也最快。我认为OP的意思是应该快速访问堆栈中存储的变量,因此CPU将尽最大努力使堆栈变量接近它,因此您希望使其变小,从而使CPU可以更快地访问变量。
157239n

1

您认为需要大量堆栈的许多事情可以通过其他方式完成。

Sedgewick的“算法”有几个很好的例子,可以通过用迭代替换递归来从递归算法(例如QuickSort)“删除”递归。实际上,该算法仍然是递归的,并且仍然是堆栈,但是您在堆上分配了排序堆栈,而不是使用运行时堆栈。

(我更喜欢第二版,在Pascal中给出了算法。它本来可以花八块钱。)

另一种看待它的方法是,如果您认为需要一个大堆栈,则您的代码效率很低。有一种更好的方法,它使用较少的堆栈。


-8

我认为没有任何技术原因,但这将是一个奇怪的应用程序,它仅在堆栈上创建了一个巨大的超级对象。堆栈对象缺乏灵活性,灵活性会随着大小的增加而变得更加成问题-您不能在不破坏它们的情况下返回并且不能将它们排队到其他线程。


1
没有人说所有对象都应该在堆栈上,我们正在讨论为什么默认堆栈很小。
Mooing Duck

不小!要用完1MB堆栈,您需要经过多少个函数调用?无论如何,默认值都可以在链接器中轻松更改,因此,剩下的是“为什么使用堆栈而不是堆?”
马丁·詹姆斯

3
一个函数调用。 int main() { char buffer[1048576]; } 这是一个非常常见的新手问题。当然,有一个简单的解决方法,但是为什么我们必须解决堆栈大小问题?
Mooing Duck

好吧,一方面,我不希望在调用受苦函数的每个线程的堆栈上造成12MB(或实际上是1MB)的堆栈需求。也就是说,我必须同意1MB有点小气。我会对默认的100MB感到满意,毕竟,没有什么可以阻止我将其降低到128K的,就像阻止其他开发人员将其提高到相同的方式一样。
马丁·詹姆斯

1
您为什么不希望在线程上造成12MB的堆栈?这样做的唯一原因是堆栈很小。那是一个递归的论点。
Mooing Duck 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.