Why is stack size in C# exactly 1 MB?

Solution 1:

enter image description here

You are looking at the guy that made that choice. David Cutler and his team selected one megabyte as the default stack size. Nothing to do with .NET or C#, this was nailed down when they created Windows NT. One megabyte is what it picks when the EXE header of a program or the CreateThread() winapi call doesn't specify the stack size explicitly. Which is the normal way, almost any programmer leaves it up the OS to pick the size.

That choice probably pre-dates the Windows NT design, history is way too murky about this. Would be nice if Cutler would write a book about it, but he's never been a writer. He's been extraordinarily influential on the way computers work. His first OS design was RSX-11M, a 16-bit operating system for DEC computers (Digital Equipment Corporation). It heavily influenced Gary Kildall's CP/M, the first decent OS for 8-bit microprocessors. Which heavily influenced MS-DOS.

His next design was VMS, an operating system for 32-bit processors with virtual memory support. Very successful. His next one was cancelled by DEC around the time the company started disintegrating, not being able to compete with cheap PC hardware. Cue Microsoft, they made him a offer he could not refuse. Many of his co-workers joined too. They worked on VMS v2, better known as Windows NT. DEC got upset about it, money changed hands to settle it. Whether VMS already picked one megabyte is something I don't know, I only know RSX-11 well enough. It isn't unlikely.

Enough history. One megabyte is a lot, a real thread rarely consumes more than a couple of handfuls of kilobytes. So a megabyte is actually rather wasteful. It is however the kind of waste you can afford on a demand-paged virtual memory operating system, that megabyte is just virtual memory. Just numbers to the processor, one each for every 4096 bytes. You never actually use the physical memory, the RAM in the machine, until you actually address it.

It is extra excessive in a .NET program because the one megabyte size was originally picked to accommodate native programs. Which tend to create large stack frames, storing strings and buffers (arrays) on the stack as well. Infamous for being a malware attack vector, a buffer overflow can manipulate the program with data. Not the way .NET programs work, strings and arrays are allocated on the GC heap and indexing is checked. The only way to allocate space on the stack with C# is with the unsafe stackalloc keyword.

The only non-trivial usage of the stack in .NET is by the jitter. It uses the stack of your thread to just-in-time compile MSIL to machine code. I've never seen or checked how much space it requires, it rather depends on the nature of the code and whether or not the optimizer is enabled, but a couple of tens of kilobytes is a rough guess. Which is otherwise how this website got its name, a stack overflow in a .NET program is quite fatal. There isn't enough space left (less than 3 kilobytes) to still reliably JIT any code that tries to catch the exception. Kaboom to desktop is the only option.

Last but not least, a .NET program does something pretty unproductive with the stack. The CLR will commit the stack of a thread. That's an expensive word that means that it doesn't just reserve the size of the stack, it also makes sure that space is reserved in the operating system's paging file so the stack can always be swapped out when necessary. Failing to commit is a fatal error and terminates a program unconditionally. That only happens on machine with very little RAM that runs entirely too many processes, such a machine will have turned to molasses before programs start dying. A possible problem 15+ years ago, not today. Programmers that tune their program to act like an F1 race-car use the <disableCommitThreadStack> element in their .config file.

Fwiw, Cutler didn't stop designing operating systems. That photo was made while he worked on Azure.


Update, I noticed that .NET no longer commits the stack. Not exactly sure when or why this happened, it's been too long since I checked. I'm guessing this design change happened somewhere around .NET 4.5. Pretty sensible change.

Solution 2:

The default reserved stack size is specified by the linker and it can be overridden by developers via changing the PE value at the link time or for an individual thread by specifying the dwStackSize parameter for the CreateThread WinAPI function.

If you create a thread with the initial stack size larger than or equal to the default stack size then it rounded up to the nearest multiple of 1 MB.

Why the value equals to 1 MB for 32-bit processes and 4 MB for 64-bit? I think you should ask developers, who designed Windows, or wait until someone of them answers your question.

Probably Mark Russinovich knows that and you can 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.

But at this time I'll try to explain some probable reasons why Microsoft have choose these values using MSDN, Mark's and Raymond's blogs.

The defaults have these values probably because in early times PCs were slow and allocating memory on the stack was much faster than allocating memory in the heap. And since stack allocations were much cheaper they were used, but it required a larger stack size.

So the value were the optimal reserved stack size for most of applications. It's optimal because allows to make a lot of nested calls and allocate memory on the stack to pass structures to calling functions. At the same time it allows to create a lot threads.

Nowadays these values are mostly used for backward compatibility, because structures which are passed as parameters to WinAPI functions are still allocated on the stack. But if you're not using stack allocations then a thread's stack usage will be significantly less than the default 1 MB and it is wasteful as Hans Passant mentioned. And to prevent this the OS commits only the first page of the stack (4 KB), if other isn't specified in the PE header of the application. Other pages are allocated on demand.

Some applications override reserved address space and initially committed to optimize memory usage. As an example, the maximum stack size of an IIS native process's thread is 256 KB (KB932909). And this decreasing of the default values is recommended by Microsoft:

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.

Sources:

  1. Thread Stack Size (Microsoft Docs)
  2. Pushing the Limits of Windows: Processes and Threads (Mark Russinovich)
  3. By default, the maximum stack size of a thread that is created in a native IIS process is 256 KB (KB932909)