Checking stack size in C#

2020-02-05 02:52发布

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

2条回答
够拽才男人
2楼-- · 2020-02-05 03:27

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.

查看更多
仙女界的扛把子
3楼-- · 2020-02-05 03:34

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.

查看更多
登录 后发表回答