在 C# 中检查堆栈大小

Posted

技术标签:

【中文标题】在 C# 中检查堆栈大小【英文标题】:Checking stack size in C# 【发布时间】:2011-02-23 11:27:31 【问题描述】:

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

【问题讨论】:

据我所知,你不能。至少不使用原生方法。 我想知道在某个时间点使用了多少堆栈。假设我调用了一个递归方法 10 次,我想知道当时使用(或留下)了多少堆叠 为此使用分析器。不要试图自己做。你的程序会用这些信息做点什么吗? 在某些情况下,知道堆栈大小会很有用。我正在研究它,因为我正在考虑嵌入一种作为编译代码运行的脚本语言,并且我想将代码插入到编译脚本中以监控和限制它自己的内存使用情况。 【参考方案1】:

这是if you have to ask, you can't afford it 的一个例子(Raymond Chen 先说。)如果代码依赖于有足够的堆栈空间,以至于它必须首先检查,那么重构它以使用显式Stack<T> 对象。 John 关于使用分析器的评论有其优点。

也就是说,事实证明有一种方法可以估计剩余的堆栈空间。这并不精确,但对于评估您离底部的距离已经足够有用了。以下内容主要基于excellent article by Joe Duffy。

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

    堆栈内存分配在一个连续的块中。 堆栈“向下”增长,从较高地址向较低地址。 系统在分配的堆栈空间底部附近需要一些空间,以允许优雅地处理堆栈外异常。我们不知道确切的保留空间,但我们会尝试保守地限制它。

有了这些假设,我们可以 pinvoke 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、调试)。给定 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 而言,每个方法调用都需要一个栈帧;但是,如果堆栈向上增长,局部变量堆栈是否与返回值堆栈分开,或者堆栈帧是否在堆上分配,它都不会在意。

【讨论】:

如果有人要求一个常数,“程序可以安全使用多少堆栈”,我会同意“IYHTA,YCAI”的理念。另一方面,如果一个人正在编写类似于解析器的东西,其中可以使用递归来处理输入上任何预期级别的嵌套结构,那么让递归检查剩余堆栈空间并调用抛出“嵌套太深”似乎更干净" 如果它不够充分,则例外,而不是对嵌套施加一些任意限制。 此检查在调试时也很有用,以便在您运行到堆栈溢出的情况下设置断点。断点将允许您转到调用堆栈的开头并检查每个变量。一旦抛出 ***Exception,Visual Studio 就无法再读取变量,为时已晚。【参考方案2】:

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

Oren 的 answer 回答了 SO 的问题(如评论所精炼),但它并未指出实际为堆栈分配了多少内存。要获得该答案,您可以使用 Michael Ganß 的答案 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 Enterprise(Build 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

鉴于这些结果与文档一致,因此没有什么特别令人震惊的。不过,有点令人惊讶的是,在 Release|Any CPU 配置中运行时,默认堆栈大小为 1 MB,带有首选 32 位选项未选中,这意味着它在 64 位操作系统上作为 64 位进程运行。我会假设这种情况下的默认堆栈大小会像 Release|x64 配置一样是 4 MB

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

【讨论】:

感谢您的发现,我也对任何 1MB 的 CPU(未选中首选 32 位选项)感到震惊。因此,即使 Environment.Is64BitProcess 为 true,它也是 1MB。 对于&lt;TargetFramework&gt;net5.0&lt;/TargetFramework&gt;(和早期版本的 .NET Core),main 的输出是“Main Stack Size : 1536 KB”。因此 .NET Core 的堆栈大小增加了 50%。但是,当我将配置更改为 Release|x64 时,该输出不会改变,这是出乎意料的。我使用 Visual Studio 中的配置管理器进行了实验。

以上是关于在 C# 中检查堆栈大小的主要内容,如果未能解决你的问题,请参考以下文章

计算方法调用堆栈大小以检查 ***Exception

C# 在运行时检查 silverlight 对象大小

在编译时检查堆栈使用情况

C# DllImport“调用导致堆栈不对称。原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配。请检查 PInvoke 签名的调用约定和参数与非托管的目标签名是否匹配 ”

C# PrintCapabilities 检查打印机是不是支持 ISOA4 页面介质大小

jvm内存堆栈监控之jmap篇