为什么 C # 中的堆栈大小恰好是1MB?

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

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

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

我对 决定背后的考虑和理由感兴趣。

41737 次浏览

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

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

为什么32位进程的值等于1MB,64位进程的值等于4MB?我认为你应该问问开发者,谁设计了 Windows,或者等到他们中有人回答你的问题。

也许马克 · 鲁辛诺维奇知道,你可以告诉他。也许你可以在他的 Windows 内部书籍早于第六版中找到这些信息,这些书描述的关于堆栈的信息比他的 文章少。或者也许 Raymond Chen 知道原因,因为他写了一些关于 Windows 内部结构和历史的有趣的东西。他也可以回答你的问题,但你应该张贴一个建议到 意见箱

但是现在我将尝试解释一些可能的原因,为什么微软使用 MSDN、 Mark 和 Raymond 的博客来选择这些值。

缺省值之所以有这些值,可能是因为在早期,PC 速度很慢,在堆栈上分配内存比在堆中分配内存快得多。由于堆栈分配要便宜得多,所以使用它们,但它需要更大的堆栈大小。

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

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

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

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

资料来源:

  1. 线程堆栈大小(MicrosoftDocs)
  2. 推进 Windows 的极限: 进程和线程(马克 · 鲁西诺维奇)
  3. 默认情况下,在本机 IIS 进程中创建的线程的最大堆栈大小为256 KB (KB932909)

enter image description here

你现在看到的就是做出这个决定的人。David Cutler 和他的团队选择了一兆字节作为默认的堆栈大小。不关你的事。NET 或者 C # ,这在他们创建 Windows NT 的时候就已经明确了。当程序的 EXE 头或 CreateThread () winapi 调用没有显式指定堆栈大小时,它会选择一兆字节。这是正常的方式,几乎任何程序员离开它的操作系统选择的大小。

这个选择可能早于 WindowsNT 的设计,历史对此太模糊了。如果 Cutler 能写本关于这个的书就好了,但他从没当过作家。他对计算机的工作方式有着非凡的影响力。他的第一个操作系统设计是 RSX-11M,一个用于 DEC 计算机(数字设备公司)的16位操作系统。它极大地影响了 Gary Kildall 的 CP/M,这是第一个用于8位微处理器的像样的操作系统。这严重影响了 MS-DOS。

他的下一个设计是 VMS,一个支持虚拟内存的32位处理器操作系统。非常成功。他的下一个计划在 DEC 公司开始分崩离析的时候被取消了,因为他们无法与廉价的 PC 硬件竞争。提示微软,他们给了他一个他无法拒绝的提议。他的许多同事也加入了。他们致力于 VMSv2,也就是众所周知的 WindowsNT。DEC 对此感到不安,于是转手换钱来解决这个问题。VMS 是否已经选择了一兆字节是我不知道的东西,我只知道 RSX-11足够好。不是不可能。

够了。一兆字节是一个 很多,一个真正的线程很少消耗超过几个千字节。所以一兆字节实际上是相当浪费的。然而,在按需分页的虚拟内存操作系统上,这种浪费是可以承受的,那兆字节只是 虚拟存储器。只有数字到处理器,每4096字节一个数字。实际上,您从来没有使用过物理内存,也就是机器中的 RAM,除非您实际地址它。

在一个。NET 程序,因为最初选择一兆字节大小是为了适应本机程序。它们倾向于创建大型堆栈帧,同时在堆栈上存储字符串和缓冲区(数组)。缓冲区溢出作为恶意软件攻击载体而臭名昭著,它可以用数据操纵程序。不是那种方式。NET 程序正常工作,字符串和数组在 GC 堆上分配,并检查索引。使用 C # 在堆栈上分配空间的唯一方法是使用不安全的 Stackalloc关键字。

中堆栈的唯一非平凡用法。NET 是由抖动。它使用线程的堆栈实时地将 MSIL 编译成机器代码。我从来没有看到或检查它需要多少空间,而是取决于代码的性质以及是否启用了优化器,但几十千字节是一个粗略的猜测。这也是这个网站得名的原因。NET 程序是相当致命的。剩下的空间不足(少于3千字节) ,无法可靠地 JIT 任何试图捕获异常的代码。到桌面是唯一的选择。

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

对了,卡特勒并没有停止设计操作系统,那张照片是他在 Azure 工作时拍的。


更新,我注意到了。NET 不再提交堆栈。不知道这是什么时候,为什么发生的,我已经很久没查过了。我猜这个设计变更发生在某个地方。NET 4.5.很明智的改变。