27

Is there a way to check threads stack size in C#?

4
  • As far as I know, you can't. At least not using a native method. Commented May 25, 2010 at 0:18
  • I would like to know how much of the stack is used at a certain point of time. Lets say I call a recursive method 10 times, I want to know how much of the stacked is used (or left) at that point Commented May 25, 2010 at 0:33
  • Use a profiler for this. Don't try to do it yourself. Is your program going to do something with this information, or what? Commented May 25, 2010 at 0:34
  • There are cases when it would be useful to know the stack size. I was researching it because I'm considering embedding a scripting language that runs as compiled code, and I want to insert code into the compiled script to monitor and limit its own memory usage. Commented Mar 27, 2021 at 14:29

2 Answers 2

25

This is a case of if you have to ask, you can't afford it (Raymond Chen said it first.) If the code depends on there being enough stack space to the extent that it has to check first, it might be worthwhile to refactor it to use an explicit Stack<T> object instead. There's merit in John's comment about using a profiler instead.

That said, it turns out that there is a way to estimate the remaining stack space. It's not precise, but it's useful enough for the purpose of evaluating how close to the bottom you are. The following is heavily based on an excellent article by Joe Duffy.

We know (or will make the assumptions) that:

  1. Stack memory is allocated in a contiguous block.
  2. The stack grows 'downwards', from higher addresses towards lower addresses.
  3. The system needs some space near the bottom of the allocated stack space to allow graceful handling of out-of-stack exceptions. We don't know the exact reserved space, but we'll attempt to conservatively bound it.

With these assumptions, we could pinvoke VirtualQuery to obtain the start address of the allocated stack, and subtract it from the address of some stack-allocated variable (obtained with unsafe code.) Further subtracting our estimate of the space the system needs at the bottom of the stack would give us an estimate of the available space.

The code below demonstrates this by invoking a recursive function and writing out the remaining estimated stack space, in bytes, as it goes:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.InteropServices; namespace ConsoleApplication1 { class Program { private struct MEMORY_BASIC_INFORMATION { public uint BaseAddress; public uint AllocationBase; public uint AllocationProtect; public uint RegionSize; public uint State; public uint Protect; public uint Type; } private const uint STACK_RESERVED_SPACE = 4096 * 16; [DllImport("kernel32.dll")] private static extern int VirtualQuery( IntPtr lpAddress, ref MEMORY_BASIC_INFORMATION lpBuffer, int dwLength); private unsafe static uint EstimatedRemainingStackBytes() { MEMORY_BASIC_INFORMATION stackInfo = new MEMORY_BASIC_INFORMATION(); IntPtr currentAddr = new IntPtr((uint) &stackInfo - 4096); VirtualQuery(currentAddr, ref stackInfo, sizeof(MEMORY_BASIC_INFORMATION)); return (uint) currentAddr.ToInt64() - stackInfo.AllocationBase - STACK_RESERVED_SPACE; } static void SampleRecursiveMethod(int remainingIterations) { if (remainingIterations <= 0) { return; } Console.WriteLine(EstimatedRemainingStackBytes()); SampleRecursiveMethod(remainingIterations - 1); } static void Main(string[] args) { SampleRecursiveMethod(100); Console.ReadLine(); } } } 

And here are the first 10 lines of output (intel x64, .NET 4.0, debug). Given the 1MB default stack size, the counts appear plausible.

969332 969256 969180 969104 969028 968952 968876 968800 968724 968648 

For brevity, the code above assumes a page size of 4K. While that holds true for x86 and x64, it might not be correct for other supported CLR architectures. You could pinvoke into GetSystemInfo to obtain the machine's page size (the dwPageSize of the SYSTEM_INFO struct).

Note that this technique isn't particularly portable, nor is it future proof. The use of pinvoke limits the utility of this approach to Windows hosts. The assumptions about the continuity and direction of growth of the CLR stack may hold true for the present Microsoft implementations. However, my (possibly limited) reading of the CLI standard (common language infrastructure, PDF, a long read) does not appear to demand as much of thread stacks. As far as the CLI is concerned, each method invocation requires a stack frame; it couldn't care less, however, if stacks grow upward, if local variable stacks are separate from return value stacks, or if stack frames are allocated on the heap.

Sign up to request clarification or add additional context in comments.

2 Comments

If one were asking for a constant number, "how much stack can a program safely use", I would agree with the "IYHTA, YCAI" philosophy. On the other hand, if one is writing something like a parser where one could use recursion to handle any expected level of nested structures on the input, it would seem cleaner to have the recursive check remaining stack space and call throw a "nesting too deep" exception if it was inadequate, than to impose some arbitrary limitation on nesting.
This check may also be useful in debugging to set a breakpoint in the very situation that you're running towards a stack overflow. A break point will allow you to go to the beginning of the call stack and inspect every variable. As soon as the StackOverflowException has been thrown, Visual Studio can't read variables anymore, it's too late.
13

I'm adding this answer for my future reference. :-)

Oren's answer answers the SO's question (as refined by the comment), but it does not indicate how much memory was actually allocated for the stack to begin with. To get that answer, you can use the Michael Ganß's answer here, which I've updated below using some more recent C# syntax.

public static class Extensions { public static void StartAndJoin(this Thread thread, string header) { thread.Start(header); thread.Join(); } } class Program { [DllImport("kernel32.dll")] static extern void GetCurrentThreadStackLimits(out uint lowLimit, out uint highLimit); static void WriteAllocatedStackSize(object header) { GetCurrentThreadStackLimits(out var low, out var high); Console.WriteLine($"{header,-19}: {((high - low) / 1024),4} KB"); } static void Main(string[] args) { WriteAllocatedStackSize("Main Stack Size"); new Thread(WriteAllocatedStackSize, 1024 * 0).StartAndJoin("Default Stack Size"); new Thread(WriteAllocatedStackSize, 1024 * 128).StartAndJoin(" 128 KB Stack Size"); new Thread(WriteAllocatedStackSize, 1024 * 256).StartAndJoin(" 256 KB Stack Size"); new Thread(WriteAllocatedStackSize, 1024 * 512).StartAndJoin(" 512 KB Stack Size"); new Thread(WriteAllocatedStackSize, 1024 * 1024).StartAndJoin(" 1 MB Stack Size"); new Thread(WriteAllocatedStackSize, 1024 * 2048).StartAndJoin(" 2 MB Stack Size"); new Thread(WriteAllocatedStackSize, 1024 * 4096).StartAndJoin(" 4 MB Stack Size"); new Thread(WriteAllocatedStackSize, 1024 * 8192).StartAndJoin(" 8 MB Stack Size"); } } 

What is interesting (and the reason I'm posting this) is the output when run using different configurations. For reference, I'm running this on a Windows 10 Enterprise (Build 1709) 64-bit OS using .NET Framework 4.7.2 (if it matters).

Release|Any CPU (Prefer 32-bit option checked):

Release|Any CPU (Prefer 32-bit option unchecked):

Release|x86:

Main Stack Size : 1024 KB Default Stack Size : 1024 KB // default stack size = 1 MB 128 KB Stack Size : 256 KB // minimum stack size = 256 KB 256 KB Stack Size : 256 KB 512 KB Stack Size : 512 KB 1 MB Stack Size : 1024 KB 2 MB Stack Size : 2048 KB 4 MB Stack Size : 4096 KB 8 MB Stack Size : 8192 KB 

Release|x64:

Main Stack Size : 4096 KB Default Stack Size : 4096 KB // default stack size = 4 MB 128 KB Stack Size : 256 KB // minimum stack size = 256 KB 256 KB Stack Size : 256 KB 512 KB Stack Size : 512 KB 1 MB Stack Size : 1024 KB 2 MB Stack Size : 2048 KB 4 MB Stack Size : 4096 KB 8 MB Stack Size : 8192 KB 

There's nothing particularly shocking about these results given that they are consistent with the documentation. What was a little bit surprising, though, was the default stack size is 1 MB when running in the Release|Any CPU configuration with the Prefer 32-bit option unchecked, meaning it runs as a 64-bit process on a 64-bit OS. I would have assumed the default stack size in this case would've been 4 MB like the Release|x64 configuration.

In any case, I hope this might be of use to someone who lands here wanting to know about the stack size of a .NET thread, like I did.

2 Comments

Thanks for your findings, I am also shocked with Any CPU (Prefer 32-bit option unchecked) coming with 1MB. So even if Environment.Is64BitProcess is true, it comes as 1MB.
For <TargetFramework>net5.0</TargetFramework> (and earlier versions of .NET Core), the output for main is "Main Stack Size : 1536 KB". So the stack size for .NET Core has increased by 50%. However, that output does not change when I change the configuration to Release|x64, which is unexpected. I did the experiment using the Configuration Manager in Visual Studio.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.