为啥每次递归都使用这么多的堆栈空间?

Posted

技术标签:

【中文标题】为啥每次递归都使用这么多的堆栈空间?【英文标题】:Why so much stack space used for each recursion?为什么每次递归都使用这么多的堆栈空间? 【发布时间】:2011-01-30 06:15:15 【问题描述】:

我有一个简单的递归函数 RCompare(),它调用一个更复杂的函数 Compare(),它在递归调用之前返回。每个递归级别使用 248 字节的堆栈空间,这似乎比它应该的要多。这是递归函数:

void CMList::RCompare(MP n1) // RECURSIVE and Looping compare function

  auto MP ne=n1->mf;
  while(StkAvl() && Compare(n1=ne->mb))
    RCompare(n1); // Recursive call !

StkAvl() 是一个简单的堆栈空间检查函数,它将自动变量的地址与存储在静态变量中的接近堆栈末尾的地址的值进行比较。

在我看来,每次递归中添加到堆栈中的唯一东西是两个指针变量(MP 是指向结构的指针)和一个函数调用存储的东西,一些保存的寄存器,基指针,返回地址等,所有 32 位(4 字节)值。不可能是248字节吧?

我不知道如何在 Visual Studio 2008 中以有意义的方式实际查看堆栈。

谢谢


添加反汇编:

CMList::RCompare:
0043E000  push        ebp  
0043E001  mov         ebp,esp 
0043E003  sub         esp,0E4h 
0043E009  push        ebx  
0043E00A  push        esi  
0043E00B  push        edi  
0043E00C  push        ecx  
0043E00D  lea         edi,[ebp-0E4h] 
0043E013  mov         ecx,39h 
0043E018  mov         eax,0CCCCCCCCh 
0043E01D  rep stos    dword ptr es:[edi] 
0043E01F  pop         ecx  
0043E020  mov         dword ptr [ebp-8],edx 
0043E023  mov         dword ptr [ebp-14h],ecx 
0043E026  mov         eax,dword ptr [n1] 
0043E029  mov         ecx,dword ptr [eax+20h] 
0043E02C  mov         dword ptr [ne],ecx 
0043E02F  mov         ecx,dword ptr [this] 
0043E032  call        CMList::StkAvl (41D46Fh) 
0043E037  test        eax,eax 
0043E039  je          CMList::RCompare+63h (43E063h) 
0043E03B  mov         eax,dword ptr [ne] 
0043E03E  mov         ecx,dword ptr [eax+1Ch] 
0043E041  mov         dword ptr [n1],ecx 
0043E044  mov         edx,dword ptr [n1] 
0043E047  mov         ecx,dword ptr [this] 
0043E04A  call        CMList::Compare (41DA05h) 
0043E04F  movzx       edx,al 
0043E052  test        edx,edx 
0043E054  je          CMList::RCompare+63h (43E063h) 
0043E056  mov         edx,dword ptr [n1] 
0043E059  mov         ecx,dword ptr [this] 
0043E05C  call        CMList::RCompare (41EC9Dh) 
0043E061  jmp         CMList::RCompare+2Fh (43E02Fh) 
0043E063  pop         edi  
0043E064  pop         esi  
0043E065  pop         ebx  
0043E066  add         esp,0E4h 
0043E06C  cmp         ebp,esp 
0043E06E  call        @ILT+5295(__RTC_CheckEsp) (41E4B4h) 
0043E073  mov         esp,ebp 
0043E075  pop         ebp  
0043E076  ret              

为什么是 0E4h?


更多信息:

class mch // match node structure

public:
    T_FSZ c1,c2;   // file indexes
    T_MSZ sz;      // match size
    enum ntyp typ; // type of node
    mch *mb,*mf;   // pointers to next and previous match nodes
;

typedef mch * MP; // for use in casting (MP) x

应该是一个普通的旧指针吧?相同的指针在结构本身中,它们只是普通的 4 字节指针。


编辑:添加:

#pragma check_stack(off)
void CMList::RCompare(MP n1) // RECURSIVE and Looping compare function

  auto MP ne=n1->mf;
  while(StkAvl() && Compare(n1=ne->mb))
    RCompare(n1); // Recursive call !
 // end RCompare()
#pragma check_stack()

但这并没有改变任何东西。 :(

现在呢?

【问题讨论】:

你能做一个 sizeof(MP) 来检查编译器认为它应该为智能指针分配多少内存来显示 MP 的定义吗? 它不是“智能指针”。尼克 D 发现了问题。 这看起来像一个调试反汇编。发布版本中是否也使用了额外的堆栈空间?您是否尝试更改编译器代码生成选项(项目属性 -> 配置属性 -> C/C++ -> 代码生成)。添加指针变量时使用的额外堆栈空间听起来像是编译器的某种缓冲区溢出检查机制。 【参考方案1】:

请注意,在调试模式下,编译器会在每个函数上绑定堆栈中的许多字节, 捕获缓冲区溢出错误。

0043E003  sub         esp, 0E4h ; < -- bound 228 bytes
...
0043E00D  lea         edi,[ebp-0E4h] 
0043E013  mov         ecx, 39h 
0043E018  mov         eax, 0CCCCCCCCh ; <-- sentinel
0043E01D  rep stos    dword ptr es:[edi] ; <-- write sentinels

编辑: OP Harvey 找到了打开/关闭堆栈探测器的编译指示。

check_stack

指示编译器关闭 如果 off(或 –)为 指定, 或打开堆栈探测器 如果指定了 on(或 +)。

#pragma check_stack([ on | off] )
#pragma check_stack+ | –

更新:嗯,探测是另一回事,正如它所显示的那样。 试试这个:/GZ (Enable Stack Frame Run-Time Error Checking)

【讨论】:

+1。根据编译器选项,它甚至可能在发布版本中这样做 好的,这听起来像是正在发生的事情。是否有 #pragma 可以在我不想要的地方关闭它? @Harvey,我不知道是否有编译指示。在发布版本中,它应该被删除。 很好,现在如何摆脱它? 啊!我发现:#pragma check_stack([ on | off] ) #pragma check_stack+ | –【参考方案2】:

在 Visual Studio 中,您可以在监视(或寄存器)窗口中查看寄存器“esp”,即堆栈指针。在你的函数中在一次调用和下一次调用之间设置一个断点,看看你消耗了多少堆栈。

在 Visual Studio 2008 的调试模式下的痛苦函数中,每个函数调用需要 16 个字节。

【讨论】:

我知道每次递归有多少...... 248 字节。但是为了什么?我相信 40 或 50。 在普通函数中是 16 个字节。它必须是 MP 变量 对不起“每个功能”。有时拼写检查器会欺骗我写一些奇怪的东西 添加一个指针参数和一个本地指针确实每次调用会增加 244 个字节。 (即使您使用字符指针,它也会这样做)。奇怪【参考方案3】:

我想必须为异常处理分配一些空间。拆机看了吗?

【讨论】:

【参考方案4】:

这也取决于您正在运行的编译器和架构 - 例如。它可以对齐到 256 字节以加快执行速度,因此每个级别使用变量的 8 字节 + 248 填充。

【讨论】:

这毫无意义,因为 248 不是 2 的幂。如果是这种情况,它应该每次调用正好使用 256 个。每次通话正好是 248,而不是 8+248。

以上是关于为啥每次递归都使用这么多的堆栈空间?的主要内容,如果未能解决你的问题,请参考以下文章

为啥递归调用会导致不同堆栈深度的 ***?

为啥增加递归深度会导致堆栈溢出错误?

如何在我的递归快速排序算法中防止堆栈溢出

为啥这个递归函数超过调用堆栈大小?

为啥 QuickSort 使用 O(log(n)) 额外空间?

如果不允许 LP 递归,那么可能会出现堆栈溢出的情况?