为什么 C# 中的堆栈大小恰好是 1 MB?
Why is stack size in C# exactly 1 MB?
今天的 PC 有大量的物理 RAM,但 C# 的堆栈大小对于 32 位进程只有 1 MB,对于 64 位进程只有 4 MB (Stack capacity in C#)。
为什么 CLR 中的堆栈大小仍然如此有限?
为什么正好是 1 MB (4 MB)(而不是 2 MB 或 512 KB)?为什么决定使用这些金额?
我对该决定背后的考虑和原因感兴趣。
默认保留堆栈大小由 linker 指定,开发人员可以通过在 link 时更改 PE 值或通过指定 dwStackSize
CreateThread
WinAPI 函数的参数。
如果您创建的线程的初始堆栈大小大于或等于默认堆栈大小,则它会四舍五入到最接近的 1 MB 倍数。
为什么 32 位进程的值等于 1 MB,64 位进程的值等于 4 MB?我认为您应该询问设计 Windows 的开发人员,或者等到他们中有人回答您的问题。
可能 Mark Russinovich 知道这一点,你可以 contact him. Maybe you can find this information in his Windows Internals books earlier than sixth edition which describes less info about stacks rather than his article. Or maybe Raymond Chen knows reasons since he writes interesting things about Windows internals and its history. He can answer your question too, but you should post a suggestion to the Suggestion Box。
但此时我将尝试使用 MSDN、Mark 和 Raymond 的博客来解释 Microsoft 选择这些值的一些可能原因。
默认值具有这些值可能是因为在早期 PC 速度很慢,并且在堆栈上分配内存比在堆中分配内存快得多。由于堆栈分配成本低得多,因此使用了它们,但它需要更大的堆栈大小。
因此该值是大多数应用程序的最佳保留堆栈大小。这是最佳的,因为允许进行大量嵌套调用并在堆栈上分配内存以将结构传递给调用函数。同时它允许创建很多线程。
现在这些值主要用于向后兼容,因为作为参数传递给 WinAPI 函数的结构仍然分配在堆栈上。但是,如果您不使用堆栈分配,那么线程的堆栈使用量将明显少于默认的 1 MB,正如 Hans Passant 提到的那样,这是一种浪费。为了防止这种情况,如果应用程序的 PE header 中未指定其他内容,OS 仅提交堆栈的第一页 (4 KB)。其他页面按需分配。
一些应用程序覆盖保留地址 space 并最初致力于优化内存使用。例如,IIS 本机进程线程的最大堆栈大小为 256 KB(Microsoft 的KB932909). And this decreasing of the default values is recommended:
It is best to choose as small a stack size as possible and commit the stack that is needed for the thread or fiber to run reliably. Every page that is reserved for the stack cannot be used for any other purpose.
来源:
你看到的是做出那个选择的人。 David Cutler 和他的团队选择 1 兆字节作为默认堆栈大小。与 .NET 或 C# 无关,这是在他们创建 Windows NT 时确定的。当程序的 EXE header 或 CreateThread() winapi 调用未明确指定堆栈大小时,它会选择 1 兆字节。这是正常的方式,几乎所有程序员都将 OS 留给它来选择大小。
那个选择可能 pre-dates Windows NT 设计,历史对此太模糊了。如果卡特勒能写一本关于它的书就好了,但他从来都不是作家。他对计算机的工作方式产生了极大的影响。他的第一个 OS 设计是 RSX-11M,一种用于 DEC 计算机(数字设备公司)的 16 位操作系统。它极大地影响了 Gary Kildall 的 CP/M,第一个不错的 8 位微处理器 OS。这对 MS-DOS.
影响很大
他的下一个设计是 VMS,这是一个支持虚拟内存的 32 位处理器操作系统。非常成功。他的下一个项目在公司开始解体时被 DEC 取消了,因为无法与廉价的 PC 硬件竞争。提示微软,他们向他提出了一个他无法拒绝的提议。他的许多 co-workers 也加入了。他们在 VMS v2 上工作,更广为人知的是 Windows NT。 DEC 对此很不高兴,钱易手来解决它。我不知道 VMS 是否已经选择了 1 兆字节,我只知道 RSX-11。这不太可能。
足够的历史。 1 MB 是 lot,一个真正的线程很少消耗超过几千字节。所以一兆字节实际上是相当浪费的。然而,在 demand-paged 虚拟内存操作系统上,这种浪费是您可以承受的,那兆字节只是 虚拟内存 。只是处理器的编号,每 4096 字节一个。在你真正解决它之前,你永远不会真正使用物理内存,机器中的 RAM。
它在 .NET 程序中显得过高,因为最初选择 1 兆字节大小是为了容纳本机程序。这往往会创建大型堆栈帧,也会在堆栈上存储字符串和缓冲区(数组)。作为恶意软件攻击媒介而臭名昭著的是,缓冲区溢出可以用数据操纵程序。不是 .NET 程序的工作方式,字符串和数组在 GC 堆上分配并检查索引。使用 C# 在堆栈上分配 space 的唯一方法是使用不安全的 stackalloc 关键字。
.NET 中堆栈的唯一 non-trivial 用法是抖动。它使用您的线程堆栈 just-in-time 将 MSIL 编译为机器码。我从未见过或检查过它需要多少 space,这取决于代码的性质以及是否启用了优化器,但粗略估计几十千字节。这就是这个网站得名的原因,.NET 程序中的堆栈溢出是非常致命的。没有足够的 space 剩余(少于 3 KB)来仍然可靠地 JIT 任何试图捕获异常的代码。 Kaboom 到桌面是唯一的选择。
最后但并非最不重要的一点是,.NET 程序对堆栈做了一些非常低效的事情。 CLR 将提交 线程的堆栈。这是一个昂贵的词,意味着它不仅保留堆栈的大小,它还确保 space 在操作系统的页面文件中保留,以便在必要时始终可以换出堆栈。未能提交是一个致命错误并无条件终止程序。这只会发生在运行太多进程的 RAM 非常小的机器上,这样的机器在程序开始死亡之前就会变成糖蜜。 15 多年前可能出现的问题,而不是今天。将程序调整为像 F1 race-car 一样运行的程序员在其 .config 文件中使用 <disableCommitThreadStack>
元素。
顺便说一下,Cutler 并没有停止设计操作系统。那张照片是他在 Azure 工作时拍摄的。
更新,我注意到 .NET 不再提交堆栈。不确定何时或为何发生这种情况,自从我检查以来已经太久了。我猜这种设计更改发生在 .NET 4.5 前后的某个地方。相当明智的改变。
今天的 PC 有大量的物理 RAM,但 C# 的堆栈大小对于 32 位进程只有 1 MB,对于 64 位进程只有 4 MB (Stack capacity in C#)。
为什么 CLR 中的堆栈大小仍然如此有限?
为什么正好是 1 MB (4 MB)(而不是 2 MB 或 512 KB)?为什么决定使用这些金额?
我对该决定背后的考虑和原因感兴趣。
默认保留堆栈大小由 linker 指定,开发人员可以通过在 link 时更改 PE 值或通过指定 dwStackSize
CreateThread
WinAPI 函数的参数。
如果您创建的线程的初始堆栈大小大于或等于默认堆栈大小,则它会四舍五入到最接近的 1 MB 倍数。
为什么 32 位进程的值等于 1 MB,64 位进程的值等于 4 MB?我认为您应该询问设计 Windows 的开发人员,或者等到他们中有人回答您的问题。
可能 Mark Russinovich 知道这一点,你可以 contact him. Maybe you can find this information in his Windows Internals books earlier than sixth edition which describes less info about stacks rather than his article. Or maybe Raymond Chen knows reasons since he writes interesting things about Windows internals and its history. He can answer your question too, but you should post a suggestion to the Suggestion Box。
但此时我将尝试使用 MSDN、Mark 和 Raymond 的博客来解释 Microsoft 选择这些值的一些可能原因。
默认值具有这些值可能是因为在早期 PC 速度很慢,并且在堆栈上分配内存比在堆中分配内存快得多。由于堆栈分配成本低得多,因此使用了它们,但它需要更大的堆栈大小。
因此该值是大多数应用程序的最佳保留堆栈大小。这是最佳的,因为允许进行大量嵌套调用并在堆栈上分配内存以将结构传递给调用函数。同时它允许创建很多线程。
现在这些值主要用于向后兼容,因为作为参数传递给 WinAPI 函数的结构仍然分配在堆栈上。但是,如果您不使用堆栈分配,那么线程的堆栈使用量将明显少于默认的 1 MB,正如 Hans Passant 提到的那样,这是一种浪费。为了防止这种情况,如果应用程序的 PE header 中未指定其他内容,OS 仅提交堆栈的第一页 (4 KB)。其他页面按需分配。
一些应用程序覆盖保留地址 space 并最初致力于优化内存使用。例如,IIS 本机进程线程的最大堆栈大小为 256 KB(Microsoft 的KB932909). And this decreasing of the default values is recommended:
It is best to choose as small a stack size as possible and commit the stack that is needed for the thread or fiber to run reliably. Every page that is reserved for the stack cannot be used for any other purpose.
来源:
你看到的是做出那个选择的人。 David Cutler 和他的团队选择 1 兆字节作为默认堆栈大小。与 .NET 或 C# 无关,这是在他们创建 Windows NT 时确定的。当程序的 EXE header 或 CreateThread() winapi 调用未明确指定堆栈大小时,它会选择 1 兆字节。这是正常的方式,几乎所有程序员都将 OS 留给它来选择大小。
那个选择可能 pre-dates Windows NT 设计,历史对此太模糊了。如果卡特勒能写一本关于它的书就好了,但他从来都不是作家。他对计算机的工作方式产生了极大的影响。他的第一个 OS 设计是 RSX-11M,一种用于 DEC 计算机(数字设备公司)的 16 位操作系统。它极大地影响了 Gary Kildall 的 CP/M,第一个不错的 8 位微处理器 OS。这对 MS-DOS.
影响很大他的下一个设计是 VMS,这是一个支持虚拟内存的 32 位处理器操作系统。非常成功。他的下一个项目在公司开始解体时被 DEC 取消了,因为无法与廉价的 PC 硬件竞争。提示微软,他们向他提出了一个他无法拒绝的提议。他的许多 co-workers 也加入了。他们在 VMS v2 上工作,更广为人知的是 Windows NT。 DEC 对此很不高兴,钱易手来解决它。我不知道 VMS 是否已经选择了 1 兆字节,我只知道 RSX-11。这不太可能。
足够的历史。 1 MB 是 lot,一个真正的线程很少消耗超过几千字节。所以一兆字节实际上是相当浪费的。然而,在 demand-paged 虚拟内存操作系统上,这种浪费是您可以承受的,那兆字节只是 虚拟内存 。只是处理器的编号,每 4096 字节一个。在你真正解决它之前,你永远不会真正使用物理内存,机器中的 RAM。
它在 .NET 程序中显得过高,因为最初选择 1 兆字节大小是为了容纳本机程序。这往往会创建大型堆栈帧,也会在堆栈上存储字符串和缓冲区(数组)。作为恶意软件攻击媒介而臭名昭著的是,缓冲区溢出可以用数据操纵程序。不是 .NET 程序的工作方式,字符串和数组在 GC 堆上分配并检查索引。使用 C# 在堆栈上分配 space 的唯一方法是使用不安全的 stackalloc 关键字。
.NET 中堆栈的唯一 non-trivial 用法是抖动。它使用您的线程堆栈 just-in-time 将 MSIL 编译为机器码。我从未见过或检查过它需要多少 space,这取决于代码的性质以及是否启用了优化器,但粗略估计几十千字节。这就是这个网站得名的原因,.NET 程序中的堆栈溢出是非常致命的。没有足够的 space 剩余(少于 3 KB)来仍然可靠地 JIT 任何试图捕获异常的代码。 Kaboom 到桌面是唯一的选择。
最后但并非最不重要的一点是,.NET 程序对堆栈做了一些非常低效的事情。 CLR 将提交 线程的堆栈。这是一个昂贵的词,意味着它不仅保留堆栈的大小,它还确保 space 在操作系统的页面文件中保留,以便在必要时始终可以换出堆栈。未能提交是一个致命错误并无条件终止程序。这只会发生在运行太多进程的 RAM 非常小的机器上,这样的机器在程序开始死亡之前就会变成糖蜜。 15 多年前可能出现的问题,而不是今天。将程序调整为像 F1 race-car 一样运行的程序员在其 .config 文件中使用 <disableCommitThreadStack>
元素。
顺便说一下,Cutler 并没有停止设计操作系统。那张照片是他在 Azure 工作时拍摄的。
更新,我注意到 .NET 不再提交堆栈。不确定何时或为何发生这种情况,自从我检查以来已经太久了。我猜这种设计更改发生在 .NET 4.5 前后的某个地方。相当明智的改变。