在堆上分配内存时,唯一的限制是可用 RAM(或虚拟内存)。它使 Gb 的内存。
那么为什么堆栈大小如此有限(大约 1 Mb)?什么技术原因阻止您在堆栈上创建非常大的对象?
更新:我的意图可能不清楚,我不想在堆栈上分配巨大的对象,我不需要更大的堆栈。这个问题只是纯粹的好奇。
在堆上分配内存时,唯一的限制是可用 RAM(或虚拟内存)。它使 Gb 的内存。
那么为什么堆栈大小如此有限(大约 1 Mb)?什么技术原因阻止您在堆栈上创建非常大的对象?
更新:我的意图可能不清楚,我不想在堆栈上分配巨大的对象,我不需要更大的堆栈。这个问题只是纯粹的好奇。
我的直觉如下。栈不像堆那么容易管理。堆栈需要存储在连续的内存位置。这意味着您不能根据需要随机分配堆栈,但您至少需要为此目的保留虚拟地址。保留的虚拟地址空间越大,可以创建的线程就越少。
例如,一个 32 位的应用程序一般有 2GB 的虚拟地址空间。这意味着如果堆栈大小为 2MB(默认为 pthreads),那么您最多可以创建 1024 个线程。这对于 Web 服务器等应用程序来说可能很小。例如,将堆栈大小增加到 100MB(即,您保留 100MB,但不必立即将 100MB 分配给堆栈),会将线程数限制为大约 20,即使对于简单的 GUI 应用程序也可能会受到限制。
一个有趣的问题是,为什么我们在 64 位平台上仍然有这个限制。我不知道答案,但我假设人们已经习惯了一些“堆栈最佳实践”:小心在堆上分配大对象,如果需要,手动增加堆栈大小。因此,没有人发现在 64 位平台上添加“巨大的”堆栈支持很有用。
还没有人提到的一个方面:
有限的堆栈大小是一种错误检测和遏制机制。
通常,C 和 C++ 中堆栈的主要工作是跟踪调用堆栈和局部变量,如果堆栈超出范围,则几乎总是设计和/或应用程序行为中的错误.
如果允许堆栈任意增长,这些错误(如无限递归)将很晚才被发现,只有在操作系统资源耗尽之后。这可以通过对堆栈大小设置任意限制来防止。实际大小并不那么重要,除了它足够小以防止系统退化。
它只是一个默认大小。如果你需要更多,你可以得到更多——通常是通过告诉链接器分配额外的堆栈空间。
拥有大堆栈的缺点是,如果您创建许多线程,它们每个都需要一个堆栈。如果所有的堆栈都在分配多 MB,但不使用它,那么空间将被浪费。
您必须为您的程序找到适当的平衡点。
有些人,比如@BJovke,认为虚拟内存本质上是免费的。确实,您不需要物理内存来支持所有虚拟内存。您必须至少能够为虚拟内存提供地址。
但是,在典型的 32 位 PC 上,虚拟内存的大小与物理内存的大小相同——因为我们只有 32 位用于任何地址,无论是否虚拟。
因为进程中的所有线程共享相同的地址空间,所以它们必须在它们之间划分地址空间。在操作系统发挥作用后,“只”有 2-3 GB 空间用于应用程序。这个大小是物理和虚拟内存的限制,因为没有更多的地址。
一方面,堆栈是连续的,因此如果您分配 12MB,则当您想要低于您创建的任何内容时,您必须删除 12MB。移动物体也变得更加困难。这是一个真实世界的示例,可能会使事情更容易理解:
假设您在房间周围堆放箱子。哪个更容易管理:
这两个例子是粗略的概括,类比中有一些明显错误的点,但它足够接近,希望它可以帮助你看到这两种情况下的优势。
按照从近到远的顺序考虑堆栈。寄存器离 CPU 很近(快),栈更远一些(但还是比较近),堆离得很远(访问慢)。
堆栈当然存在于堆上,但由于它被连续使用,它可能永远不会离开 CPU 缓存,使其比普通的堆访问更快。这是保持堆栈大小合理的原因;尽可能保持缓存。分配大堆栈对象(可能会在溢出时自动调整堆栈大小)违背了这一原则。
所以它是一个很好的性能范例,而不仅仅是旧时代的遗留物。
例如,在 100MB 的堆栈中分配大对象会使大多数机器无法将它们一次加载到缓存中,这几乎违背了堆栈的目的。
堆栈的要点是将属于同一范围的小对象(因此通常需要一起或彼此靠近)一起存储在连续的内存地址中,以便程序可以将它们全部加载到缓存中同时,最大限度地减少缓存未命中,一般来说,CPU 必须等待的时间,直到它从较慢的 RAM 中获取一些丢失的数据。
存储在堆栈中的 50MB 对象不适合缓存,这意味着在每个缓存行之后会有一个 CPU 等待时间,直到从 RAM 中取出下一条数据,这意味着一个会阻塞调用堆栈并且没有任何意义与从堆中加载相比,受益(在速度方面)。
许多你认为需要大量筹码的事情可以通过其他方式完成。
Sedgewick 的“算法”有几个很好的例子,通过用迭代替换递归,从递归算法(如 QuickSort)中“删除”递归。实际上,算法仍然是递归的,并且仍然存在 as 堆栈,但是您将排序堆栈分配在堆上,而不是使用运行时堆栈。
(我喜欢第二版,用 Pascal 给出算法。它可以用 8 美元。)
另一种看待它的方式是,如果您认为需要一个大堆栈,那么您的代码效率低下。有一种使用更少堆栈的更好方法。
如果您可以有一个无限堆栈,那么每个虚拟地址都可能被堆栈使用。如果堆栈可以使用每个地址,那么堆就没有地方可去。您为堆变量选择的每个地址都可能被不断增长的堆栈覆盖。
换句话说,栈上的变量和堆上的变量占用同一个虚拟地址空间。我们需要某种方法来防止堆分配器在堆栈可能增长的地方分配数据。堆栈大小是一种简单的方法。堆分配器知道堆栈地址已被占用,因此它使用其他东西。
我不认为有任何技术原因,但它会是一个奇怪的应用程序,它只是在堆栈上创建了一个巨大的超级对象。堆栈对象缺乏灵活性,随着大小的增加,问题变得更加严重——你不能在不破坏它们的情况下返回,也不能将它们排队到其他线程。