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

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

您可以为每个线程单独设置堆栈大小,尽管 Delphi TThread 实现并未将其表面化(请参见 QC #77203),而不是更改全局设置。 - user160694
2
这是一篇有关编程的文章,链接为QC77203: http://qc.embarcadero.com/wc/qcmain.aspx?d=77203 - Johan
6个回答

12

使用此方法来计算当前线程栈所占用的内存大小:

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;

我还有另一个想法。


@opc0de:为什么你使用Pastebin而不是将代码嵌入到SO中? - Jens Mühlenhoff
@Jens Mühlenhoff,Pastebin相比本地嵌入的代码块有许多优点,包括但不限于行号和正确的语法高亮显示。 - Premature Optimization
@Johan 你是对的,Johan,但根据我的经验,修改ebx寄存器不会导致应用程序崩溃,但我会进行修改。 - opc0de
4
Opc0de,能否在那段代码中添加一些注释来解释它正在做什么? - Rob Kennedy
1
@user,必须要从SO中跳出来查看代码的想法令人不安!特别是对于只有7行代码的情况。这是一个经典的“强行加意愿”的例子(你可能想要行号和语法高亮显示,所以我将其放在外部网站X上),即使我从未关心过这些事情。 - Johan
显示剩余13条评论

9
为了完整起见,我添加了一个版本的CommittedStackSize函数,该函数提供了opc0de的答案,用于确定在Windows的x86 32位和64位版本下都能工作的已用堆栈大小。opc0de的函数仅适用于Win32。

opc0de的函数查询堆栈基址和最低提交堆栈基址的地址,这些信息来自Windows的线程信息块(TIB)。 x86和x64之间有两个区别:

  • 在Win32上,TIB由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;

3

我记得几年前在初始化时用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)

我依稀记得那个时候曾与Kim Kokkonen合作过,我想原始代码是出自他手。

这种方法的好处是你没有性能损失,并且在程序运行期间没有任何分析操作。只有在关闭程序时,直到找到不同值为止的循环代码才会消耗CPU周期。(后来我们用汇编语言编写了那段代码。)


1

即使8个线程都接近使用其1MB的堆栈,那也仅占用8MB的虚拟内存。如果我没记错,线程的默认初始堆栈大小为64K,除非达到进程线程堆栈限制,否则增加页面故障,此时我假设您的进程将停止并显示'Stack overflow'消息框:((

我担心减少进程堆栈限制$MAXSTACKSIZE不会缓解您的碎片化/分页问题,甚至可能起不了任何作用。您需要更多的RAM,以便您mega-photo-app的常驻页面集更大,从而降低抖动。

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

敬礼, 马丁


0

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

使用32位Delphi,您拥有最大地址空间为4GB,因此这确实在某种程度上限制了您。您可能需要为某些或所有线程使用较小的堆栈,但是在足够大的机器上仍将面临问题。

如果您希望帮助您的应用程序更好地适应更大的机器,则可能需要采取以下一项或多项措施:

  1. 避免创建比核心显着更多的线程。 使用可用于您的插件的线程池体系结构。 没有 .net 环境的好处来轻松完成此操作,您最好针对 Windows 线程池 API 进行编码。 也就是说,肯定有一个很好的 Delphi 封装可用。
  2. 处理内存分配模式。 如果您的线程正在分配大约200MB的连续块,则这将给您的分配器带来过多的压力。 我发现最好是将这样大量的内存分配分配给较小的固定大小块。 这种方法解决了您遇到的碎片问题。

线程池绝对是未来开发的正确选择。至于将图像分割成块:这会使任何基于Gr32的图像处理代码无法工作或变得更加复杂(比如在基于瓷砖的图像上渲染文本)。 - Steffen Binas

0

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

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

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

至少,这是一个理论,不知道是否真正有效...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接