Проверка размера стека в C # - PullRequest
12 голосов
/ 25 мая 2010

Есть ли способ проверить размер стека потоков в C #?

Ответы [ 2 ]

16 голосов
/ 25 мая 2010

Это случай , если вам нужно спросить, вы не можете себе это позволить (Рэймонд Чен сказал это первым.) Если код зависит от наличия достаточного стекового пространства в той степени, в которой он имеет чтобы проверить сначала, возможно, стоит реорганизовать его для использования явного объекта Stack<T>. В комментарии Джона есть смысл использовать профилировщик.

Тем не менее, оказывается, что есть способ оценить оставшееся пространство стека. Это не точно, но достаточно полезно для оценки того, насколько вы близки к основанию. Нижеследующее в значительной степени основано на превосходной статье Джо Даффи .

Мы знаем (или сделаем предположения), что:

  1. Память стека выделяется в непрерывном блоке.
  2. Стек увеличивается «вниз», от старших адресов к младшим.
  3. Системе требуется некоторое пространство в нижней части выделенного стекового пространства, чтобы обеспечить плавную обработку исключений вне стека. Мы не знаем точно зарезервированное пространство, но мы попытаемся его консервативно связать.

С этими допущениями мы могли бы определить 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). С учетом размера стека по умолчанию в 1 МБ счетчик выглядит правдоподобно.

969332
969256
969180
969104
969028
968952
968876
968800
968724
968648

Для краткости приведенный выше код предполагает размер страницы 4K. Хотя это справедливо для x86 и x64, это может быть неправильно для других поддерживаемых архитектур CLR. Вы можете ввести GetSystemInfo , чтобы получить размер страницы машины (dwPageSize структуры SYSTEM_INFO ).

Обратите внимание, что эта техника не особенно переносима и не является перспективной. Использование pinvoke ограничивает полезность этого подхода для хостов Windows. Предположения о непрерывности и направлении роста стека CLR могут быть верны для настоящих реализаций Microsoft. Однако мое (возможно ограниченное) чтение стандарта CLI (общеязыковая инфраструктура, PDF, длинное чтение), по-видимому, не требует столько стеков потоков. Что касается CLI, то для каждого вызова метода требуется кадр стека; однако это не должно волновать, если стеки растут вверх, если стеки локальных переменных отделены от стеков возвращаемых значений, или если кадры стека расположены в куче.

1 голос
/ 12 декабря 2018

Я добавляю этот ответ для моей будущей ссылки. : -)

Oren's answer отвечает на вопрос SO (как уточнено в комментарии), но не указывает, сколько памяти фактически было выделено для стека для начала. Чтобы получить этот ответ, вы можете использовать ответ Михаила Ганса здесь , который я обновил ниже, используя более поздний синтаксис 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");
    }
}

Что интересно (и причина, по которой я это публикую) - это вывод при запуске с использованием разных конфигураций. Для справки, я запускаю это на 64-битной ОС Windows 10 Enterprise (сборка 1709), используя .NET Framework 4.7.2 (если это имеет значение).

Release | Любой ЦП (предпочитается 32-битная опция проверено ):

Release | Любой ЦП (предпочитают 32-битную опцию не проверено ):

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

Нет ничего особенно шокирующего в этих результатах, учитывая, что они соответствуют документации. Что было немного удивительно, так это то, что размер стека по умолчанию составляет 1 МБ при работе в Release | Любая конфигурация ЦП с опцией Prefer 32-bit unchecked , что означает, что он работает как 64-разрядный процесс в 64-разрядной ОС. Я бы предположил, что размер стека по умолчанию в этом случае был бы 4 МБ , как в конфигурации Release | x64.

В любом случае, я надеюсь, что это может пригодиться тому, кто сюда захочет узнать размер стека потока .NET, как я.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...