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

143

今天的个人电脑虽然有大量的物理内存,但是C#的堆栈大小在32位进程中只有1 MB,在64位进程中为4 MB (C#中的堆栈容量)。

为什么CLR中的堆栈大小还是如此有限呢?

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

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


8
64位进程的默认堆栈大小为4MB,32位进程为1MB。您可以通过更改PE头中的值来修改主线程的堆栈大小。您也可以使用Thread构造函数的适当重载来指定堆栈大小。但是,这引出了一个问题,为什么需要更大的堆栈? - Yuval Itzchakov
2
谢谢,已编辑。 :) 问题不是关于如何使用更大的堆栈大小,而是为什么决定将堆栈大小设为1MB(4MB)。 - Nikolay Kostov
11
每个线程默认都会获得这个栈大小,而大多数线程并不需要那么大的空间。我刚刚开机,系统目前运行了1200个线程。现在做个简单的计算吧 ;) - Lucas Trzesniewski
2
@LucasTrzesniewski 不仅如此,它还必须在内存中具有传染性。请注意,堆栈大小越大,您的进程在其虚拟地址空间中创建的线程就越少。 - Yuval Itzchakov
不确定“确切的”1 MB:在我的Windows 8.1上,一个.NET Core 3.1控制台应用程序具有1572864字节的默认堆栈大小(使用GetCurrentThreadStackLimits Win32 API检索)。我能够使用stackalloc大约1500000字节而不会出现StackOverflowException。 - Giorgi Chakhidze
显示剩余2条评论
2个回答

266

enter image description here

您正在查看做出该选择的人。 David Cutler和他的团队将一兆字节选为默认堆栈大小。这与.NET或C#无关,而是在创建Windows NT时确定的。当程序的EXE头文件或CreateThread()winapi调用未明确指定堆栈大小时,它会选择1MB。这是正常方式,几乎所有程序员都会让操作系统选择大小。
这个选择可能早于Windows NT设计,历史太过混沌。如果Cutler能够写一本关于它的书就好了,但他从来没有成为一名作家。他对计算机工作方式的影响非常大。他的第一个操作系统设计是RSX-11M,这是DEC计算机(Digital Equipment Corporation)的16位操作系统。它对Gary Kildall的CP / M产生了很大影响,这是第一个适用于8位微处理器的良好操作系统。这严重影响了MS-DOS。
His next design was VMS, an operating system for 32-bit processors with virtual memory support. It was very successful. His next project was cancelled by DEC around the time the company started disintegrating, as they were unable to compete with cheap PC hardware. Microsoft made him an offer he could not refuse, and many of his co-workers joined too. They worked on VMS v2, better known as Windows NT. DEC was upset about it and money changed hands to settle it. Whether VMS had already reached one megabyte is something I don't know, but I am familiar enough with RSX-11. This is enough history. One megabyte is a lot, as a real thread rarely consumes more than a couple of kilobytes. So a megabyte is actually quite wasteful. However, it is the kind of waste that can be afforded on a demand-paged virtual memory operating system, as that megabyte is just virtual memory - just numbers to the processor, one for every 4096 bytes. Physical memory, or the RAM in the machine, is never actually used until it is addressed.
它在.NET程序中显得特别过度,因为最初选择了1MB大小以适应本地程序。这些程序往往会在栈上创建大型堆栈帧,将字符串和缓冲区(数组)存储在栈上。由于成为恶意软件攻击向量而臭名昭著,缓冲区溢出可以操纵数据的程序。.NET程序的工作方式与此不同,字符串和数组分配在GC堆上并进行索引检查。使用C#在堆栈上分配空间的唯一方法是使用不安全的stackalloc关键字。
.NET中栈的唯一非平凡用法是Jitter。它使用您的线程的堆栈将MSIL即时编译为机器代码。我从未见过或检查过它需要多少空间,这取决于代码的性质以及是否启用了优化器,但大约需要几十KB。否则,这就是此网站如何得到其名称的原因,.NET程序中的堆栈溢出非常致命。剩余空间不足(小于3KB)以仍然可靠地JIT尝试捕获异常的任何代码。桌面上的Kaboom是唯一的选择。
最后但并非最不重要的,一个.NET程序在堆栈方面做了一些相当低效的事情。CLR会提交一个线程的堆栈。这是一个昂贵的词,意思是它不仅保留了堆栈的大小,还确保在操作系统的分页文件中保留了空间,以便在必要时可以始终交换出堆栈。未能提交是致命错误,并无条件地终止程序。这只会发生在内存很少的机器上运行过多进程的情况下,这样的机器在程序开始死亡之前就会变得像糖浆一样。15年前可能存在的问题,但今天已不再是问题。将他们的程序调整为像F1赛车一样运行的程序员,在其.config文件中使用<disableCommitThreadStack>元素。
顺便说一句,卡特尔并没有停止设计操作系统。那张照片是在他工作于Azure期间拍的。

更新,我注意到.NET不再提交堆栈。不确定何时或为什么发生了这种情况,因为我很久没有检查了。我猜测这种设计变更大约发生在.NET 4.5左右。这是一个相当明智的变化。


6
关于你的评论“在C#中,唯一使用不安全的stackalloc关键字来分配栈空间。” - 在方法内声明的局部变量(例如int)不是存储在堆栈上吗?我认为它们是的。 - RBT
2
好的。现在我明白了,栈帧不是函数局部变量存储的唯一选择。正如你在其中一个要点中建议的那样,它可以存储在栈帧上。非常有启发性的Hans。我无法表达对你撰写如此富有洞见的文章的感激之情。老实说,栈对于编程来说是一个如此重要的抽象,只是为了避免不必要的复杂性。 - RBT
非常详细的描述,@Hans。我只是想知道线程的maxStackSize的最小可能值是多少?我在MSDN上找不到它。根据您的评论,堆栈使用是绝对最小的,我可以使用最小值来容纳最大可能的线程。谢谢。 - MKR
1
@HansPassant 我来这里是想知道递归需要多深才会导致堆栈溢出。换句话说,我想知道,在问题规模O(n)的情况下,递归深度应该是多少才合适。因此,我对一个具有20个int局部变量 + 参数的递归函数进行了快速计算,使用了一个4MB堆栈程序:4*1024*1024 / (4*20) = 52429 - 因此,对于这样一个函数,它可以深入52,429级而不会发生堆栈溢出。我的计算接近真相吗? - KFL
2
@KFL:你可以尝试一下来回答自己的问题! - Eric Lippert
2
如果默认行为不再提交堆栈,则需要编辑此Markdown文件:https://github.com/dotnet/docs/blob/master/docs/framework/configure-apps/file-schema/runtime/disablecommitthreadstack-element.md - John Stewien

6
默认保留的堆栈大小由链接器指定,可以通过在链接时更改PE值或为单个线程指定CreateThread WinAPI函数的dwStackSize参数来被开发者覆盖。
如果您创建的线程初始堆栈大小大于或等于默认堆栈大小,则会将其舍入到最接近的1 MB的倍数。
为什么32位进程的值等于1 MB,而64位进程的值等于4 MB?我认为您应该问设计Windows的开发人员,或者等待他们中的某个人回答您的问题。
我翻译如下:

可能马克·鲁西诺维奇知道这个问题,你可以通过联系方式与他取得联系。也许你可以在他的“Windows Internals”系列书籍中找到相关信息,该书的前六版对堆栈的描述比较少,而他的文章则更详细。或者雷蒙德·陈可能也知道原因,因为他写了有关Windows内部和历史的有趣事情。他也能回答你的问题,但你应该在建议箱中发布建议。

但是现在我会尝试解释Microsoft为什么选择这些值的一些可能原因,这些信息来自于MSDN、马克和雷蒙德的博客。

默认值可能是这些值,因为早期PC运行速度较慢,分配堆栈内存比分配堆内存快得多。由于堆栈分配要便宜得多,因此被使用,但需要更大的堆栈大小。

对于大多数应用程序而言,最佳保留堆栈大小是这些值的优化。它是最优的,因为允许进行许多嵌套调用并在堆栈上分配内存以将结构传递给调用函数。同时,它也允许创建很多线程。
如今,这些值大多用于向后兼容性,因为作为WinAPI函数参数传递的结构仍然是在堆栈上分配的。但是,如果您不使用堆栈分配,那么线程的堆栈使用量将显著低于默认值1 MB,这会浪费资源,就像Hans Passant所提到的一样。为了防止这种情况,操作系统只会提交堆栈的第一页(4 KB),除非在应用程序的PE头中指定了其他页。
有些应用程序覆盖保留地址空间并最初提交以优化内存使用。例如,IIS本机进程线程的最大堆栈大小为256 KB (KB932909)。Microsoft 建议 减少默认值。
最好选择尽可能小的堆栈大小,并提交线程或纤程需要运行可靠的堆栈。为堆栈保留的每个页面都不能用于任何其他目的。
来源:
  1. 线程堆栈大小(Microsoft Docs)
  2. 推动Windows的极限:进程和线程(Mark Russinovich)
  3. 默认情况下,在本机IIS进程中创建的线程的最大堆栈大小为256 KB(KB932909)

如果我想要一个更大的堆栈大小,我可以进行设置(http://www.atalasoft.com/cs/blogs/rickm/archive/2008/04/22/increasing-the-size-of-your-stack-net-memory-management-part-3.aspx)。 我想了解该决策背后的考虑和原因。 - Nikolay Kostov
2
好的,现在我明白了 :) 默认堆栈大小应该是最优的(请参见@Lucas Trzesniewski的评论),并且应该舍入到最接近分配粒度的倍数。如果指定的堆栈大小大于默认堆栈大小,则将其舍入到最接近1MB的倍数。因此,Microsoft选择这些大小作为所有用户模式应用程序的默认堆栈大小。没有其他原因。 - Yoh Deadfall
有任何来源吗?有任何文档资料吗? :) - Nikolay Kostov
@Yoh 有趣的链接。你应该把它总结到你的答案中。 - Lucas Trzesniewski
@NikolayKostov针对那些需要的人,这是由NikolayKostov提供的链接:https://web.archive.org/web/20150224144103/http://www.atalasoft.com/cs/blogs/rickm/archive/2008/04/22/increasing-the-size-of-your-stack-net-memory-management-part-3.aspx - zeroG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接