Что такое безопасный максимальный размер стека или как измерить использование стека? - PullRequest
8 голосов
/ 27 мая 2011

У меня есть приложение с несколькими рабочими потоками, по одному на каждое ядро.На современном 8-ядерном компьютере у меня есть 8 таких потоков.Мое приложение загружает много плагинов, которые также имеют свои собственные рабочие потоки.Поскольку приложение использует огромные блоки памяти (фотографии, например, 200 МБ), у меня проблема фрагментации памяти.Проблема в том, что каждый поток выделяет {$ MAXSTACKSIZE ...} адресного пространства.Он использует не физическую память, а адресное пространство.Я уменьшил MAXSTACKSIZE с 1 МБ до 128 КБ, и это, кажется, работает, но я не могу, если я близок к пределу.Есть ли возможность измерить, сколько реально используется стек?

Ответы [ 6 ]

11 голосов
/ 27 мая 2011

Используйте это, чтобы вычислить объем памяти, выделенный для стека текущего потока:

function CommittedStackSize: Cardinal;
asm
  mov eax,[fs:$4] // base of the stack, from the Thread Environment Block (TEB)
  mov edx,[fs:$8] // address of lowest committed stack page
                  // this gets lower as you use more stack
  sub eax,edx
end;

Еще одна идея, которой у меня нет.

7 голосов
/ 19 января 2014

Для полноты картины я добавляю версию функции CommittedStackSize, предоставленную в ответе opc0de для определения количества используемого стека , который будет работать как для x86 32- и 64-битные версии Windows (функция opc0de предназначена только для Win32).

Функция opc0de запрашивает адрес базы стека и наименьшую фиксированную базу стека из блока информации о потоке Windows (TIB)) .Между x86 и x64 есть два различия:

  • На TIB указывает регистр сегмента FS на Win32, но на GS на Win64 (см. здесь )
  • Абсолютные смещения элементов в структуре различаются (в основном потому, что некоторые элементы являются указателями, т.е. 4 байта и 8 байтов на Win32 / 64 соответственно)

Кроме того, обратите внимание, что естьнебольшая разница в коде BASM, потому что на x64 требуется abs, чтобы ассемблер использовал абсолютное смещение от регистра сегмента.

Следовательно, версия, которая будет работать как на Win32, так и на Win64 версиивыглядит так:

{$IFDEF MSWINDOWS}
function CommittedStackSize: NativeUInt;
//NB: Win32 uses FS, Win64 uses GS as base for Thread Information Block.
asm
 {$IFDEF WIN32}
  mov eax, [fs:04h] // TIB: base of the stack
  mov edx, [fs:08h] // TIB: lowest committed stack page
  sub eax, edx      // compute difference in EAX (=Result)
 {$ENDIF}
 {$IFDEF WIN64}
  mov rax, abs [gs:08h] // TIB: base of the stack
  mov rdx, abs [gs:10h] // TIB: lowest committed stack page
  sub rax, rdx          // compute difference in RAX (=Result)
 {$ENDIF}
{$ENDIF}
end;
3 голосов
/ 27 мая 2011

Я помню, я заполнял все доступное пространство стека с нулями в начале года назад и подсчитывал непрерывные нули в deinit, начиная с конца.Это дало хорошую «высокую отметку», при условии, что вы отправите свое приложение через все шаги для пробных запусков.

Я выкопаю код, когда вернусь немобильным.

Обновление: ОКпринцип продемонстрирован в этом (древнем) коде:

{***********************************************************
  StackUse - A unit to report stack usage information

  by Richard S. Sadowsky
  version 1.0 7/18/88
  released to the public domain

  Inspired by a idea by Kim Kokkonen.

  This unit, when used in a Turbo Pascal 4.0 program, will
  automatically report information about stack usage.  This is very
  useful during program development.  The following information is
  reported about the stack:

  total stack space
  Unused stack space
  Stack spaced used by your program

  The unit's initialization code handles three things, it figures out
  the total stack space, it initializes the unused stack space to a
  known value, and it sets up an ExitProc to automatically report the
  stack usage at termination.  The total stack space is calculated by
  adding 4 to the current stack pointer on entry into the unit.  This
  works because on entry into a unit the only thing on the stack is the
  2 word (4 bytes) far return value.  This is obviously version and
  compiler specific.

  The ExitProc StackReport handles the math of calculating the used and
  unused amount of stack space, and displays this information.  Note
  that the original ExitProc (Sav_ExitProc) is restored immediately on
  entry to StackReport.  This is a good idea in ExitProc in case a
  runtime (or I/O) error occurs in your ExitProc!

  I hope you find this unit as useful as I have!

************************************************************)

{$R-,S-} { we don't need no stinkin range or stack checking! }
unit StackUse;

interface

var
  Sav_ExitProc     : Pointer; { to save the previous ExitProc }
  StartSPtr        : Word;    { holds the total stack size    }

implementation

{$F+} { this is an ExitProc so it must be compiled as far }
procedure StackReport;

{ This procedure may take a second or two to execute, especially }
{ if you have a large stack. The time is spent examining the     }
{ stack looking for our init value ($AA). }

var
  I                : Word;

begin
  ExitProc := Sav_ExitProc; { restore original exitProc first }

  I := 0;
  { step through stack from bottom looking for $AA, stop when found }
  while I < SPtr do
    if Mem[SSeg:I] <> $AA then begin
      { found $AA so report the stack usage info }
      WriteLn('total stack space : ',StartSPtr);
      WriteLn('unused stack space: ', I);
      WriteLn('stack space used  : ',StartSPtr - I);
      I := SPtr; { end the loop }
    end
    else
      inc(I); { look in next byte }
end;
{$F-}


begin
  StartSPtr := SPtr + 4; { on entry into a unit, only the FAR return }
                         { address has been pushed on the stack.     }
                         { therefore adding 4 to SP gives us the     }
                         { total stack size. }
  FillChar(Mem[SSeg:0], SPtr - 20, $AA); { init the stack   }
  Sav_ExitProc := ExitProc;              { save exitproc    }
  ExitProc     := @StackReport;          { set our exitproc }
end.

http://webtweakers.com/swag/MEMORY/0018.PAS.html)

. Я слабо помню, как работал с Ким Кокконен в то время, и я думаю, что оригинальный кодот него.

Хорошая вещь в этом подходе в том, что у вас нулевое снижение производительности и нет операций профилирования во время выполнения программы. Только после выключения кода цикла до тех пор, пока не будет найдено значение, которое израсходует циклы ЦП. (Мы запрограммировали его позже в сборке.)

1 голос
/ 27 мая 2011

Даже если все 8 потоков приблизятся к использованию 1 МБ стека, это всего лишь 8 МБ виртуальной памяти.IIRC, начальный размер стека для потоков по умолчанию составляет 64 КБ, увеличиваясь при сбоях страниц, если не достигнут предел стека потоков процесса, и в этот момент я предполагаю, что ваш процесс будет остановлен с помощью MessageBox «Переполнение стека»: ((

Я боюсь, что уменьшение предела стека процессов $ MAXSTACKSIZE не сильно облегчит вашу проблему фрагментации / разбиения на страницы, если что-нибудь. Вам нужно больше оперативной памяти, чтобы резидентный набор страниц вашего мега-фото-приложения был больше и, таким образом, уменьшался трах.

Сколько потоков в среднем в вашем процессе? Диспетчер задач может показать это.

Rgds, Martin

0 голосов
/ 27 мая 2011

Сокращение $ MAXSTACKSIZE не будет работать, потому что Windows всегда будет выравнивать стек потоков до 1 МБ (?).

Один (возможный?) Способ предотвратить фрагментацию - зарезервировать (не выделять!) Виртуальную память (с помощью VirtualAlloc) перед созданием потоков. И отпустите его после запуска потоков. Таким образом, Windows не может использовать зарезервированное пространство для потоков, поэтому у вас будет некоторая непрерывная память.

Или вы можете создать свой собственный менеджер памяти для больших фотографий: зарезервируйте много виртуальной памяти и выделите память из этого пула вручную. (вам нужно вести список использованной и использованной памяти самостоятельно).

По крайней мере, это теория, не знаю, работает ли она на самом деле ...

0 голосов
/ 27 мая 2011

Хотя я уверен, что вы можете уменьшить размер стека в вашем приложении, я не думаю, что это устранит основную причину проблемы. Сейчас вы используете 8-ядерный компьютер, но что происходит на 16-ядерном или 32-ядерном компьютере и т. Д.

С 32-битным Delphi у вас максимальное адресное пространство 4 ГБ, и это в некоторой степени ограничивает вас. Возможно, вам придется использовать меньшие стеки для некоторых или всех ваших потоков, но вы все равно столкнетесь с проблемами на достаточно большой машине.

Если вы помогаете вашему приложению лучше масштабироваться на более крупные машины, вам может потребоваться выполнить один или другой из следующих шагов:

  1. Избегайте создания значительно большего количества потоков, чем ядер. Используйте архитектуру пула потоков, доступную для ваших плагинов. Без использования среды .net, которая облегчит эту задачу, вы будете лучше всего кодировать с помощью API пула потоков Windows. Тем не менее, должна быть хорошая доступная оболочка Delphi.
  2. Работа с шаблонами распределения памяти. Если ваши потоки распределяют смежные блоки в области 200 МБ, то это вызовет чрезмерную нагрузку на ваш распределитель. Я обнаружил, что часто лучше распределять такие большие объемы памяти в меньших блоках фиксированного размера. Этот подход обходит проблемы фрагментации, с которыми вы сталкиваетесь.
...