119

今天的 PC 有大量的物理 RAM,但 C# 的堆栈大小对于 32 位进程只有 1 MB,对于 64 位进程只有 4 MB(C# 中的堆栈容量)。

为什么 CLR 中的堆栈大小仍然如此有限?

为什么它正好是 1 MB (4 MB)(而不是 2 MB 或 512 KB)?为什么决定使用这些金额?

我对这个决定背后的考虑和原因很感兴趣。

4

2 回答 2

237

在此处输入图像描述

你正在看那个做出这个选择的人。David Cutler 和他的团队选择 1 MB 作为默认堆栈大小。与 .NET 或 C# 无关,这是在他们创建 Windows NT 时确定的。当程序的 EXE 标头或 CreateThread() winapi 调用未明确指定堆栈大小时,它会选择一兆字节。这是正常的方式,几乎所有程序员都让操作系统来选择大小。

这种选择可能早于 Windows NT 设计,历史对此太模糊了。如果卡特勒能写一本关于它的书就好了,但他从来都不是作家。他对计算机的工作方式具有非凡的影响力。他的第一个操作系统设计是 RSX-11M,一个用于 DEC 计算机(Digital Equipment Corporation)的 16 位操作系统。它极大地影响了 Gary Kildall 的 CP/M,这是第一个体面的 8 位微处理器操作系统。这严重影响了 MS-DOS。

他的下一个设计是 VMS,一个支持虚拟内存的 32 位处理器操作系统。非常成功。他的下一个在公司开始解体时被 DEC 取消,无法与廉价的 PC 硬件竞争。提示微软,他们给了他一个他无法拒绝的提议。他的许多同事也加入了。他们在 VMS v2 上工作,更广为人知的是 Windows NT。DEC对此感到不安,钱转手来解决它。我不知道 VMS 是否已经选择了 1 兆字节,我只对 RSX-11 足够了解。这并非不可能。

足够的历史。一兆字节很多,一个真正的线程很少消耗超过几千字节。所以一兆字节实际上是相当浪费的。然而,在按需分页的虚拟内存操作系统上,这是您可以承受的那种浪费,即兆字节只是虚拟内存。只是给处理器编号,每 4096 个字节一个。在你真正解决它之前,你永远不会真正使用物理内存,即机器中的 RAM。

它在 .NET 程序中是多余的,因为最初选择 1 兆字节的大小是为了容纳本机程序。这往往会创建大型堆栈帧,也会在堆栈上存储字符串和缓冲区(数组)。作为恶意软件攻击媒介而臭名昭著,缓冲区溢出可以用数据操纵程序。不是 .NET 程序的工作方式,在 GC 堆上分配字符串和数组并检查索引。使用 C# 在堆栈上分配空间的唯一方法是使用 unsafe stackalloc关键字。

.NET 中堆栈的唯一重要用途是抖动。它使用线程堆栈将 MSIL 即时编译为机器代码。我从未见过或检查过它需要多少空间,这取决于代码的性质以及是否启用了优化器,但几十千字节是一个粗略的猜测。这就是这个网站得名的方式,.NET 程序中的堆栈溢出是非常致命的。没有足够的空间(小于 3 KB)来仍然可靠地 JIT 任何试图捕获异常的代码。Kaboom 到桌面是唯一的选择。

最后但并非最不重要的一点是,.NET 程序对堆栈做了一些非常低效的事情。CLR 将提交线程的堆栈。这是一个昂贵的词,意味着它不仅保留堆栈的大小,还确保在操作系统的页面文件中保留空间,以便在必要时始终可以换出堆栈。未能提交是一个致命错误,并无条件终止程序。这只发生在运行太多进程的 RAM 非常少的机器上,这样的机器在程序开始死亡之前就会变成糖蜜。15 多年前可能存在的问题,而不是今天。将程序调整为像 F1 赛车一样工作的程序员会使用<disableCommitThreadStack>.config 文件中的元素。

Fwiw,卡特勒并没有停止设计操作系统。这张照片是他在 Azure 上工作时拍摄的。


更新,我注意到 .NET 不再提交堆栈。不完全确定发生这种情况的时间或原因,自从我检查以来已经太久了。我猜这种设计更改发生在 .NET 4.5 附近。相当明智的改变。

于 2015-02-22T13:07:30.067 回答
6

默认保留堆栈大小由链接器指定,开发人员可以通过在链接时更改 PE 值或通过指定WinAPI 函数的dwStackSize参数来覆盖单个线程。CreateThread

如果您创建的线程的初始堆栈大小大于或等于默认堆栈大小,则它会向上舍入到最接近的 1 MB 倍数。

为什么 32 位进程的值等于 1 MB 而 64 位进程的值等于 4 MB?我认为您应该询问设计 Windows 的开发人员,或者等到他们中的某个人回答您的问题。

马克 Russinovich 可能知道这一点,您可以联系他。也许您可以在早于第六版的他的 Windows Internals 书籍中找到这些信息,其中描述的有关堆栈的信息比他的文章要少。或者,也许 Raymond Chen 知道原因,因为他写了有关 Windows 内部结构及其历史的有趣内容。他也可以回答你的问题,但你应该在Suggestion Box中发表建议。

但此时,我将尝试解释 Microsoft 使用 MSDN、Mark 和 Raymond 的博客选择这些值的一些可能原因。

默认值具有这些值可能是因为早期 PC 速度较慢,并且在堆栈上分配内存比在堆中分配内存要快得多。而且由于堆栈分配便宜得多,因此它们被使用,但它需要更大的堆栈大小。

因此,该值是大多数应用程序的最佳保留堆栈大小。这是最佳的,因为允许进行大量嵌套调用并在堆栈上分配内存以将结构传递给调用函数。同时它允许创建很多线程。

如今,这些值主要用于向后兼容,因为作为参数传递给 WinAPI 函数的结构仍然分配在堆栈上。但是,如果您不使用堆栈分配,那么线程的堆栈使用量将大大低于默认的 1 MB,并且正如 Hans Passant 所提到的那样是浪费的。为了防止这种情况,操作系统只提交堆栈的第一页(4 KB),如果在应用程序的 PE 标头中没有指定其他页面。其他页面按需分配。

一些应用程序会覆盖保留的地址空间并最初致力于优化内存使用。例如,IIS 本机进程线程的最大堆栈大小为 256 KB ( KB932909 )。微软建议减少默认值:

最好选择尽可能小的堆栈大小,并提交线程或光纤可靠运行所需的堆栈。为堆栈保留的每个页面都不能用于任何其他目的。

资料来源:

  1. 线程堆栈大小 (Microsoft Docs)
  2. 突破 Windows 的极限:进程和线程 (Mark Russinovich)
  3. 默认情况下,在本机 IIS 进程中创建的线程的最大堆栈大小为 256 KB (KB932909)
于 2015-02-22T10:53:19.583 回答