什么是安全的最大堆栈大小或如何测量堆栈的使用



我有一个应用程序,它有许多工作线程,每个核心一个。在一台现代化的8芯机器上,我有8根这样的螺纹。我的应用程序加载了许多插件,这些插件也有自己的工作线程。由于该应用程序使用巨大的内存块(照片,例如200 MB),我有内存碎片问题(32位应用程序)。问题是每个线程都分配{$MAXSTACKSIZE…}的地址空间。它不是使用物理内存,而是使用地址空间。我将MAXSTACKSIZE从1MB减少到128KB,它似乎有效,但我不知道我是否接近极限。有没有可能测量实际使用了多少堆栈?

使用它来计算为当前线程堆栈提交的内存量:

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;

还有一个我没有的主意。

为了完整起见,我添加了一个版本的CommittedStackSize函数,该函数在opc0de的答案中提供,用于确定所使用的堆栈数量,该堆栈适用于x86 32位和64位版本的Windows(opc0de函数仅适用于Win32)。

opc0de的函数从Window的线程信息块(TIB)中查询堆栈的基址和最低提交的堆栈基址。x86和x64之间有两个区别:

  • TIB由Win32上的FS段寄存器指向,但由Win64上的GS指向(请参阅此处)
  • 结构中项目的绝对偏移量不同(主要是因为有些项目是指针,即Win32/64上分别为4字节和8字节)

另外请注意,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;

我记得几年前,I FillChar在初始化时用零计算了所有可用的堆栈空间,并在去初始化时从末尾开始计算连续的零。这产生了一个很好的"高水位线",前提是你发送你的应用程序进行探测。

当我回到非机动状态时,我会找出代码。

更新:好的,原理在这个(古老的)代码中得到了证明:

{***********************************************************
  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)

我隐约记得当时曾与金·科科宁合作过,我认为原始代码来自他。

这种方法的好处是在程序运行过程中没有性能损失,也没有分析操作。只有在关闭循环直到找到更改的值时,代码才会占用CPU周期。(我们稍后在汇编中对其进行了编码。)

即使所有8个线程都接近使用1MB的堆栈,也只有8MB的虚拟内存。IIRC,线程的默认初始堆栈大小为64K,在出现页面错误时会增加,除非达到进程线程堆栈限制,此时我假设您的进程将停止,并显示"堆栈溢出"消息框:(

我担心降低进程堆栈限制$MAXSTACKSIZE不会大大缓解您的碎片/分页问题。你需要更多的RAM,这样你的巨型照片应用程序的常驻页面集就更大了;因此减少了颠簸。

在您的进程中,平均总共有多少个线程?任务管理器可以显示这一点。

Rgds,Martin

虽然我确信你可以减少应用程序中的线程堆栈大小,但我认为这不会解决问题的根本原因。你现在使用的是8核机器,但在16核或32核等上会发生什么

使用32位Delphi,您的最大地址空间为4GB,因此这在一定程度上限制了您。您可能需要对部分或全部线程使用较小的堆栈,但在足够大的机器上仍然会遇到问题。

如果你帮助你的应用程序更好地扩展到更大的机器,你可能需要采取以下一个或另一个步骤:

  1. 避免创建明显多于核心的线程。使用可用于插件的线程池体系结构。如果没有.net环境的优势使这一切变得容易,你将是最好的编码Windows线程池API。也就是说,必须有一个好的Delphi包装可用
  2. 处理内存分配模式。如果您的线程正在分配200MB范围内的连续块,那么这将给您的分配器带来不必要的压力。我发现,通常最好将如此大的内存分配在更小、固定大小的块中。这种方法可以解决您遇到的碎片化问题

减少$MAXSTACKSIZE不起作用,因为Windows总是将线程堆栈对齐到1Mb(?)。

防止碎片的一种(可能的?)方法是在创建线程之前保留(而不是分配!)虚拟内存(使用VirtualAlloc)。并在线程运行后释放它。这样Windows就无法使用线程的保留空间,因此您将拥有一些连续内存。

或者,你可以为大型照片制作自己的内存管理器:保留大量虚拟内存,并手动从这个池中分配内存。(您需要自己维护一份已使用和已使用内存的列表)。

至少,这是一个理论,不知道它是否真的有效。。。

最新更新