为啥堆栈内存大小如此有限?
Posted
技术标签:
【中文标题】为啥堆栈内存大小如此有限?【英文标题】:why is stack memory size so limited?为什么堆栈内存大小如此有限? 【发布时间】:2012-05-16 00:19:35 【问题描述】:当您在堆上分配内存时,唯一的限制是可用 RAM(或虚拟内存)。它使 Gb 的内存。
那么为什么堆栈大小如此有限(大约 1 Mb)?什么技术原因阻止您在堆栈上创建非常大的对象?
更新:我的意图可能不清楚,我不想在堆栈上分配巨大的对象,我不需要更大的堆栈。这个问题纯属好奇。
【问题讨论】:
为什么在堆上创建大对象是可行的? (调用链通常在堆栈中。) 我认为真正的答案比大多数答案所描绘的要简单:“因为我们一直都是这样做的,而且到目前为止一切都很好,为什么要改变呢?” @JerryCoffin 您是否阅读了迄今为止发布的任何答案?这个问题有更深入的了解。 @user1202136:我已经阅读了所有这些 - 但人们在猜测,我的猜测是,他们引用的许多因素可能在做出最初决定时都没有考虑到主题。创造一个短语,“有时雪茄只是雪茄。” "我们应该将默认堆栈设置为多大?" “哦,我不知道,我们可以运行多少个线程?” “它在超过 K 的地方爆炸” “好的,那么,我们称之为 2K,我们有 2 Gig 的虚拟,那么 1 meg 怎么样?” “是的,好的,下一个问题是什么?” 【参考方案1】:在一个 100MB 的堆栈中分配大对象将使得大多数机器无法将它们一次加载到缓存中,这几乎违背了堆栈的目的。
堆栈的意义在于将属于同一范围的小对象(因此,通常需要在一起或彼此靠近)一起存储在连续的内存地址中,以便程序可以将它们全部加载同时进入缓存,最大限度地减少缓存未命中,一般来说,CPU 必须等待的时间,直到它从较慢的 RAM 中获取一些丢失的数据。
堆栈中存储的 50MB 对象无法放入缓存中,这意味着在每个缓存行之后,CPU 都会等待一段时间,直到从 RAM 中取出下一条数据,这意味着会阻塞调用堆栈而不是与从堆中加载相比,获得任何显着优势(在速度方面)。
【讨论】:
【参考方案2】:这只是一个默认大小。如果您需要更多,您可以获得更多 - 通常是通过告诉链接器分配额外的堆栈空间。
拥有大堆栈的缺点是,如果您创建许多线程,每个线程都需要一个堆栈。如果所有的堆栈都在分配多MB,但没有使用它,就会浪费空间。
您必须为您的程序找到适当的平衡点。
有些人,比如@BJovke,认为虚拟内存本质上是免费的。确实,您不需要物理内存来支持所有虚拟内存。您必须至少能够提供虚拟内存的地址。
但是,在典型的 32 位 PC 上,虚拟内存的大小与物理内存的大小相同 - 因为我们对于任何地址(无论是否虚拟)都只有 32 位。
因为一个进程中的所有线程共享相同的地址空间,所以它们必须在它们之间进行划分。在操作系统发挥作用后,“只”有 2-3 GB 空间用于应用程序。而这个大小是both物理和虚拟内存的限制,因为没有更多的地址了。
【讨论】:
最大的线程问题是你不能轻易地向其他线程发送堆栈对象的信号。要么生产者线程必须同步等待消费者线程释放对象,要么必须制作昂贵且会产生争用的深层副本。 @MartinJames:没有人说所有对象都应该在堆栈上,我们正在讨论为什么默认堆栈大小很小。 空间不会被浪费,栈大小只是对连续虚拟地址空间的保留。因此,如果您将堆栈大小设置为 100 MB,那么将实际使用的 RAM 量取决于线程中的堆栈消耗。 @BJovke - 但是 virtual 地址空间仍将被用完。在 32 位进程中,这被限制为几 GB,因此仅保留 20*100MB 会给您带来问题。【参考方案3】:您认为需要大量筹码的许多事情可以通过其他方式完成。
Sedgewick 的“算法”有几个很好的例子,它们通过用迭代替换递归,从递归算法(如 QuickSort)中“移除”递归。现实中算法还是递归的,还有as栈,只是你在堆上分配排序栈,而不是使用运行时栈。
(我喜欢第二版,用 Pascal 给出算法。八块钱就可以用了。)
另一种看待它的方式是,如果您认为需要一个大堆栈,那么您的代码效率低下。有一种使用更少堆栈的更好方法。
【讨论】:
【参考方案4】:还没有人提到的一个方面:
有限的堆栈大小是一种错误检测和遏制机制。
通常,C 和 C++ 中堆栈的主要工作是跟踪调用堆栈和局部变量,如果堆栈超出范围,则几乎总是设计和/或行为中的错误应用程序。
如果允许堆栈任意增长,这些错误(如无限递归)将很晚才被发现,只有在操作系统资源耗尽之后。这可以通过对堆栈大小设置任意限制来防止。实际大小并不那么重要,只是它足够小以防止系统退化。
【讨论】:
您可能对分配的对象有类似的问题(因为替换递归的某种方法是手动处理堆栈)。该限制强制使用其他方式(不需要更安全/更简单/..)(注意关于(玩具)列表实现的注释数量std::unique_ptr
编写析构函数(而不依赖于智能指针))。 【参考方案5】:
按照从近到远的顺序考虑堆栈。寄存器离 CPU 近(快),栈远一点(但还是比较近),堆离得很远(访问慢)。
堆栈当然存在于堆上,但由于它被连续使用,它可能永远不会离开 CPU 缓存,使其比普通的堆访问更快。 这是保持堆栈大小合理的原因;尽可能保持缓存。分配大堆栈对象(可能会在溢出时自动调整堆栈大小)违背了这一原则。
所以它是一个很好的性能范例,而不仅仅是旧时代的遗留物。
【讨论】:
虽然我确实相信缓存在人为减少堆栈大小的原因中起着重要作用,但我必须纠正您关于“堆栈存在于堆上”的说法。堆栈和堆都存在于内存中(虚拟或物理)。 “近还是远”与访问速度有什么关系? @MinhNghĩa 好吧,RAM 中的变量被缓存在 L2 内存中,然后被缓存在 L1 内存中,然后甚至那些被缓存在寄存器中。访问 RAM 很慢,访问 L2 更快,L1 仍然更快,而寄存器最快。我认为 OP 的意思是应该快速访问存储在堆栈中的变量,因此 CPU 将尽最大努力使堆栈变量靠近它,因此您希望它变小,因此 CPU 可以更快地访问变量。 【参考方案6】:我的直觉如下。栈不像堆那么容易管理。堆栈需要存储在连续的内存位置。这意味着您不能根据需要随机分配堆栈,但您至少需要为此目的保留虚拟地址。保留的虚拟地址空间越大,可以创建的线程就越少。
例如,一个 32 位的应用程序一般有 2GB 的虚拟地址空间。这意味着如果堆栈大小为 2MB(在 pthreads 中是默认值),那么您最多可以创建 1024 个线程。这对于 Web 服务器等应用程序来说可能很小。例如,将堆栈大小增加到 100MB(即,您保留 100MB,但不一定立即将 100MB 分配给堆栈),会将线程数限制为大约 20,即使对于简单的 GUI 应用程序也可能会受到限制。
一个有趣的问题是,为什么我们在 64 位平台上仍然有这个限制。我不知道答案,但我假设人们已经习惯了一些“堆栈最佳实践”:小心在堆上分配大对象,如果需要,手动增加堆栈大小。因此,没有人发现在 64 位平台上添加“巨大”堆栈支持很有用。
【讨论】:
许多 64 位机器只有 48 位地址(与 32 位相比获得了很大的收益,但仍然有限)。即使有额外的空间,您也必须担心与页表相关的保留方式——也就是说,拥有更多空间总是有开销。分配一个新的段(mmap)而不是为每个线程保留巨大的堆栈空间,即使不是更便宜,也可能同样便宜。 @edA-qamort-ora-y:这个答案不是在谈论 allocation,而是在谈论 virtual memory reserveration,这几乎是免费,而且肯定比 mmap 快很多。【参考方案7】:我不认为有任何技术原因,但它会是一个奇怪的应用程序,它只是在堆栈上创建了一个巨大的超级对象。堆栈对象缺乏灵活性,随着大小的增加,问题变得更加严重 - 不销毁它们就不能返回,也不能将它们排队到其他线程。
【讨论】:
没有人说所有对象都应该在堆栈上,我们正在讨论为什么默认堆栈大小很小。 不小!你需要通过多少次函数调用才能用完 1MB 的堆栈?无论如何,默认值在链接器中很容易更改,因此,我们留下了“为什么使用堆栈而不是堆?” 一个函数调用。int main() char buffer[1048576];
这是一个很常见的新手问题。当然有一个简单的解决方法,但为什么我们必须解决堆栈大小问题?
好吧,一方面,我不希望对调用受影响函数的每个线程的堆栈造成 12MB(或者实际上是 1MB)的堆栈要求。也就是说,我不得不承认 1MB 有点小气。我会对默认的 100MB 感到满意,毕竟,没有什么能阻止我把它调低到 128K,就像没有什么能阻止其他开发者调高它一样。
你为什么不想在你的线程上造成 12MB 的堆栈?唯一的原因是堆栈很小。这是一个递归论证。【参考方案8】:
一方面,堆栈是连续的,所以如果您分配 12MB,当您想要低于您创建的任何内容时,您必须删除 12MB。移动物体也变得更加困难。这是一个现实世界的例子,可能会让事情更容易理解:
假设您在房间周围堆放箱子。哪个更容易管理:
将任何重量的箱子堆叠在一起,但是当您需要在底部放一些东西时,您必须撤消整堆。如果您想从堆中取出一件物品并交给其他人,您必须取下所有盒子并将盒子移到其他人的堆中(仅限堆叠) 你把你所有的盒子(除了非常小的盒子)放在一个特殊的区域,不要把东西堆在其他东西上面,然后在一张纸上写下你放在哪里(一个指针),然后放纸堆上。如果您需要将盒子交给其他人,您只需将纸叠中的纸条递给他们,或者只需给他们一张纸的复印件并将原件留在您的纸堆中。 (栈 + 堆)这两个例子是粗略的概括,在类比中有一些明显错误的点,但它足够接近,希望它能帮助你看到这两种情况的优势。
【讨论】:
@MooingDuck 是的,但是您在程序的虚拟内存中工作,如果我进入一个子程序,在堆栈上放一些东西,然后从子程序返回,我需要取消分配或移动我在展开堆栈以返回到我原来的位置之前创建的对象。 虽然我的评论是由于误解(我删除了它),但我仍然不同意这个答案。从栈顶移除 12MB 实际上是一个操作码。它基本上是免费的。编译器也可以并且确实欺骗了“堆栈”规则,所以不,他们不必在展开之前复制/移动对象以返回它。所以我认为你的评论也不正确。 嗯,释放 12MB 占用堆栈上的一个操作码超过 100 个堆上的操作码通常并不重要 - 它可能低于实际处理 12MB 缓冲区的噪音水平。如果编译器在注意到返回了一个大得离谱的对象时想要作弊(例如,通过在调用之前移动 SP 以使对象空间成为调用者堆栈的一部分),那么这很好,TBH,返回此类的开发人员对象(而不是指针/引用)在一定程度上受到编程挑战。 @MartinJames:C++ 规范还说该函数通常可以将数据直接放入目标缓冲区而不使用临时缓冲区,因此如果您小心,返回 12MB 缓冲区没有开销按价值。以上是关于为啥堆栈内存大小如此有限?的主要内容,如果未能解决你的问题,请参考以下文章