3

Consider the following sample:

 static void DoNothing()
 {
     signal.WaitOne();
 }

 static void Main(string[] args)
 {
     signal = new ManualResetEvent(false);
     List<Thread> threads = new List<Thread>();
     try
     {
         while (true)
         {
             Console.WriteLine($"{threads.Count}, Memory:{Process.GetCurrentProcess().PrivateMemorySize64 / (1024 * 1024)}");
             var thread = new Thread(DoNothing);
             thread.Start();
             threads.Add(thread);
         }
     }
     catch (OutOfMemoryException)
     {
         Console.WriteLine($"Out of memory at: {threads.Count}");
         signal.Set();
     }
     threads.ForEach(t => t.Join());
     Console.WriteLine("Finished.");
     Console.ReadLine();
 }

The code is compiled as a 32-bit process.

I discovered it behaves differently when compiled for .NET 3.5, and for 4.x. I only change the version of the Target framework.

When compiled for NET 3.5, the memory is exhausted with approx. 1 MB per thread created. This is as expected, because the default stack size is 1MB (https://msdn.microsoft.com/en-us/library/windows/desktop/ms686774(v=vs.85).aspx)

However, when compiled for .NET 4.x, the memory is consumed as pace of approx. 100KB per thread created, i.e. 1/10th of 1MB.

Did the default stack size changed between .NET 3.5 and 4.x?

I conduct the experiment on Windows 10. Is it possible this has to do with the version of Windows?

Nick
  • 4,787
  • 2
  • 18
  • 24
  • Do you have all the updates for Net 3.5? There were a few minor changes between 3.5 and 4.0 with defaults. I'm not sure if this size is one of them. Again from 4.0 to 4.5 there were some upgrades. I would think the size is based on the max size for a 32 bit address. If you are using Windows 7 there is a 32 bit version and a 64 bit version. The 32 bit version was a beta version and most installations went to 64 bit. A 32 bit process will handle only ~2M signed and ~4M unsigned. – jdweng Dec 12 '16 at 13:26
  • stack size is allocated based on `IMAGE_OPTIONAL_HEADER` the linker write to `SizeOfStack*` based on option - `/STACK:reserve[,commit]` - https://msdn.microsoft.com/en-us/library/8cxs58a6.aspx - however when we create thread - we can overwrite defaults – RbMm Dec 12 '16 at 13:30

2 Answers2

4

In the meanwhile, I found the answer.

The stack size is not changes, it is 1 MB across all versions.

What got changed is obviously the way memory is committed. With .NET 4.x, seems only 100K of each stack is actually committed (i.e. consumed, more information can be found in the Tomes of Wisdom). The rest is just reserved in case it's actually needed later. The app still loses 1MB of its virtual memory and crashes at about 1450 threads created, regardless of the version of the Framework.

Thanks to Hans Passant for the tip: https://stackoverflow.com/a/28658130/383426

Community
  • 1
  • 1
Nick
  • 4,787
  • 2
  • 18
  • 24
2

According to MSDN:

https://msdn.microsoft.com/en-us/library/5cykbwz4.aspx?tduid=(b5f758a58cbf5c5c52da174750c6fbc0)(256380)(2459594)(TnL5HPStwNw-EgIcu96.E0oIiXcj83I4gQ)()

The minimum stack size of each thread is ~256KB and the default is 1MB. The space must be contiguous, so if you increase the stack size for a thread, you will be able to run fewer total threads in your application.

The default thread stack size has not changed. The only thing to my knowledge that has changed is that as of .Net 4.5, perhaps earlier, .Net no longer commits the stack of a thread (When it did commit, it didn't just reserve the size of the stack, it also made sure that space was reserved in the operating system's paging file so the stack could always be swapped out when necessary.)

However, for your example, this is all irrelevant! Why? The main thread in a 32 bit .Net process receives 1MB of stack space. The overhead for EACH thread is not 1MB, in fact, it is fairly small, being in the KB range. They each get their own stack space, which is independent of the main threads stack space.

I am not sure what you are referencing by "..with approx. 1 MB per thread created".

You can calculate the stack size remaining to you by using some unsafe code

class Program
{
static int n;
static int topOfStack;
const int stackSize = 1000000; // Default?

// The func is 76 bytes, but we need space to unwind the exception.
const int spaceRequired = 18*1024; 

unsafe static void Main(string[] args)
{
    int var;
    topOfStack = (int)&var;

    n=0;
    recurse();
}

unsafe static void recurse()
{
    int remaining;
    remaining = stackSize - (topOfStack - (int)&remaining);
    if (remaining < spaceRequired)
        throw new Exception("Cheese");
    n++;
    recurse();
}
}
Keith
  • 1,119
  • 2
  • 12
  • 23