在C#中检查堆栈大小

时间:2010-05-24 23:58:36

标签: c# multithreading stack

有没有办法在C#中检查线程堆栈大小?

2 个答案:

答案 0 :(得分:15)

这是if you have to ask, you can't afford it的情况(Raymond Chen首先说过。)如果代码依赖于有足够的堆栈空间到必须首先检查的程度,那么重构它可能是值得使用的而是一个明确的Stack<T>对象。约翰关于使用分析器的评论是有用的。

也就是说,事实证明有一种方法可以估算剩余的堆栈空间。它不精确,但它足够有用,可以评估你的底部有多接近底部。以下内容主要基于excellent article by Joe Duffy

我们知道(或将作出假设):

  1. 堆栈内存分配在一个连续的块中。
  2. 从高位地址到低地址,堆栈“向下”增长。
  3. 系统需要在分配的堆栈空间底部附近留出一些空间,以便正常处理堆栈外异常。我们不知道确切的保留空间,但我们会尝试保守地约束它。
  4. 通过这些假设,我们可以调整VirtualQuery来获取分配的堆栈的起始地址,并从一些堆栈分配的变量的地址中减去它(使用不安全的代码获得。)进一步减去我们对系统在堆栈底部需要的空间将为我们提供可用空间的估计。

    下面的代码通过调用递归函数并写出剩余的估计堆栈空间(以字节为单位)来演示这一点:

    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();
            }
        }
    }
    

    以下是前10行输出(intel x64,.NET 4.0,debug)。鉴于1MB的默认堆栈大小,计数似乎是合理的。

    969332
    969256
    969180
    969104
    969028
    968952
    968876
    968800
    968724
    968648
    

    为简洁起见,上面的代码假定页面大小为4K。虽然这适用于x86和x64,但对于其他受支持的CLR架构可能不正确。您可以转到GetSystemInfo以获取计算机的页面大小(SYSTEM_INFO结构的dwPageSize)。

    请注意,此技术不是特别便携,也不是未来的证据。 pinvoke的使用限制了此方法对Windows主机的实用性。关于CLR堆栈的连续性和增长方向的假设可能适用于当前的Microsoft实现。但是,我(可能有限)阅读CLI standard(公共语言基础设施,PDF,长读)似乎并不需要那么多的线程堆栈。就CLI而言,每个方法调用都需要一个堆栈帧;但是,如果堆栈向上增长,如果局部变量堆栈与返回值堆栈分开,或者堆栈帧是否在堆上分配,那么它可能并不在意。

答案 1 :(得分:0)

我正在添加此答案以供将来参考。 :-)

Oren的answer回答了SO的问题(通过注释进行了细化),但是它没有指出实际上为堆栈开始分配了多少内存。要获得该答案,可以使用MichaelGanß的答案here,下面我使用一些最新的C#语法对其进行了更新。

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");
    }
}

有趣的(以及我发布此消息的原因)是使用不同配置运行时的输出。作为参考,我正在使用.NET Framework 4.7.2在Windows 10企业版(内部版本1709)64位操作系统上运行它(如果有的话)。

发布|任何CPU(首选32位选项已选中):

发布|任何CPU(首选32位选项未选中):

发布| 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

发布| 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

鉴于这些结果与文档一致,因此没有特别令人震惊的地方。但是,令人惊讶的是,在带有“首选32位”选项的Release | Any CPU配置中运行时,默认堆栈大小为 1 MB 未经检查的,表示它在64位操作系统上作为64位进程运行。在这种情况下,我假设默认的堆栈大小是 4 MB ,就像Release | x64配置一样。

无论如何,我希望这对像我一样希望了解.NET线程的堆栈大小的人有用。