简体   繁体   English

有多少递归函数调用会导致堆栈溢出?

[英]how many recursive function calls causes stack overflow?

I am working on a simulation problem written in c, the main part of my program is a recursive function.我正在研究一个用 c 编写的模拟问题,我的程序的主要部分是一个递归函数。 when the recursive depth reaches approximately 500000 it seems stack overflow occurs.当递归深度达到大约 500000 时,似乎发生堆栈溢出。

Q1 : I want to know that is this normal? Q1 : 我想知道这正常吗?

Q2 : in general how many recursive function calls causes stack overflow? Q2 :一般有多少递归函数调用会导致堆栈溢出?

Q3 : in the code below, removing local variable neighbor can prevent from stack overflow? Q3 : 在下面的代码中,去除局部变量neighbor可以防止堆栈溢出吗?

my code:我的代码:

/*
 * recursive function to form Wolff Cluster(= WC)
 */
void grow_Wolff_cluster(lattic* l, Wolff* wolff, site *seed){

    /*a neighbor of site seed*/
    site* neighbor;

    /*go through all neighbors of seed*/
    for (int i = 0 ; i < neighbors ; ++i) {


        neighbor = seed->neighbors[i];

        /*add to WC according to the Wolff Algorithm*/
        if(neighbor->spin == seed->spin && neighbor->WC == -1 && ((double)rand() / RAND_MAX) < add_probability)
        {
            wolff->Wolff_cluster[wolff->WC_pos] = neighbor;
            wolff->WC_pos++;                  // the number of sites that is added to WC
            neighbor->WC = 1;          // for avoiding of multiple addition of site
            neighbor->X = 0;


            ///controller_site_added_to_WC();


            /*continue growing Wolff cluster(recursion)*/
            grow_Wolff_cluster(l, wolff, neighbor);
        }
    }
}

I want to know that is this normal?我想知道这正常吗?

Yes.是的。 There's only so much stack size.只有这么多的堆栈大小。

In the code below, removing local variable neighbor can prevent from stack overflow?在下面的代码中,删除局部变量邻居可以防止堆栈溢出吗?

No. Even with no variables and no return values the function calls themselves must be stored in the stack so the stack can eventually be unwound.不。即使没有变量和返回值,函数调用本身也必须存储在堆栈中,以便最终可以展开堆栈。

For example...例如...

void recurse() {
    recurse();
}

int main (void)
{
    recurse();
}

This still overflows the stack.这仍然会溢出堆栈。

$ ./test
ASAN:DEADLYSIGNAL
=================================================================
==94371==ERROR: AddressSanitizer: stack-overflow on address 0x7ffee7f80ff8 (pc 0x00010747ff14 bp 0x7ffee7f81000 sp 0x7ffee7f81000 T0)
    #0 0x10747ff13 in recurse (/Users/schwern/tmp/./test+0x100000f13)

SUMMARY: AddressSanitizer: stack-overflow (/Users/schwern/tmp/./test+0x100000f13) in recurse
==94371==ABORTING
Abort trap: 6

In general how many recursive function calls causes stack overflow?一般来说,有多少递归函数调用会导致堆栈溢出?

That depends on your environment and function calls.这取决于您的环境和函数调用。 Here on OS X 10.13 I'm limited to 8192K by default.在 OS X 10.13 上,我默认限制为 8192K。

$ ulimit -s
8192

This simple example with clang -g can recurse 261976 times.这个带有clang -g简单示例可以递归 261976 次。 With -O3 I can't get it to overflow, I suspect compiler optimizations have eliminated my simple recursion.使用-O3我不能让它溢出,我怀疑编译器优化已经消除了我的简单递归。

#include <stdio.h>

void recurse() {
    puts("Recurse");
    recurse();
}

int main (void)
{
    recurse();
}

Add an integer argument and it's 261933 times.添加一个整数参数,它是 261933 次。

#include <stdio.h>

void recurse(int cnt) {
    printf("Recurse %d\n", cnt);
    recurse(++cnt);
}

int main (void)
{
    recurse(1);
}

Add a double argument, now it's 174622 times.添加一个双参数,现在是 174622 次。

#include <stdio.h>

void recurse(int cnt, double foo) {
    printf("Recurse %d %f\n", cnt, foo);
    recurse(++cnt, foo);
}

int main (void)
{
    recurse(1, 2.3);
}

Add some stack variables and it's 104773 times.添加一些堆栈变量,它是 104773 次。

#include <stdio.h>

void recurse(int cnt, double foo) {
    double this = 42.0;
    double that = 41.0;
    double other = 40.0;
    double thing = 39.0;
    printf("Recurse %d %f %f %f %f %f\n", cnt, foo, this, that, other, thing);
    recurse(++cnt, foo);
}

int main (void)
{
    recurse(1, 2.3);
}

And so on.等等。 But I can increase my stack size in this shell and get twice the calls.但是我可以在这个 shell 中增加我的堆栈大小并获得两倍的调用。

$ ./test 2> /dev/null | wc -l
174622
$ ulimit -s 16384
$ ./test 2> /dev/null | wc -l
349385

I have a hard upper limit to how big I can make the stack of 65,532K or 64M.对于 65,532K 或 64M 的堆栈,我有一个严格的上限。

$ ulimit -Hs
65532
  1. Yes and no - if you come across a stack overflow in your code, it could mean a few things是与否 - 如果您在代码中遇到堆栈溢出,这可能意味着一些事情

    • Your algorithm is not implemented in a way that respects the amount of memory on the stack you have been given.您的算法没有以尊重您获得的堆栈上的内存量的方式实现。 You may adjust this amount to suit the needs of the algorithm.您可以调整此数量以满足算法的需要。

      If this is the case, it's more common to change the algorithm to more efficiently utilize the stack, rather than add more memory.如果是这种情况,更常见的是更改算法以更有效地利用堆栈,而不是添加更多内存。 Converting a recursive function to an iterative one, for example, saves a lot of precious memory.例如,将递归函数转换为迭代函数可以节省大量宝贵的内存。

    • It's a bug trying to eat all your RAM.这是一个试图吃掉你所有内存的错误。 You forgot a base case in the recursion or mistakenly called the same function.您忘记了递归中的基本情况或错误地调用了相同的函数。 We've all done it at least 2 times.我们都至少做过2次。

  2. It's not necessarily how many calls cause an overflow - it's dependent upon how much memory each individual call takes up on a stack frame.不一定有多少调用会导致溢出 - 它取决于每个单独调用在堆栈帧上占用多少内存。 Each function call uses up stack memory until the call returns.每个函数调用都用完堆栈内存,直到调用返回。 Stack memory is statically allocated -- you can't change it at runtime (in a sane world).堆栈内存是静态分配的——你不能在运行时改变它(在一个理智的世界里)。 It's a last-in-first-out (LIFO) data structure behind the scenes.这是幕后的后进先出 (LIFO) 数据结构。

  3. It's not preventing it, it's just changing how many calls to grow_Wolff_cluster it takes to overflow the stack memory.它并没有阻止它,它只是改变了溢出堆栈内存所需的对grow_Wolff_cluster调用grow_Wolff_cluster On a 32-bit system, removing neighbor from the function costs a call to grow_Wolff_cluster 4 bytes less.在 32 位系统上,从函数中删除neighbor的调用成本grow_Wolff_cluster 4 个字节。 It adds up quickly when you multiply that in the hundreds of thousands.当您将其乘以数十万时,它会迅速加起来。

I suggest you learn more about how stacks work for you.我建议您了解更多有关堆栈如何为您工作的信息。 Here's a good resource over on the software engineering stack exchange. 这是关于软件工程堆栈交换的一个很好的资源 And another here on stack overflow (zing!)另一个堆栈溢出(zing!)

A stack overflow isn't defined by the C standard, but by the implementation.堆栈溢出不是由 C 标准定义的,而是由实现定义的。 The C standard defines a language with unlimited stack space (among other resources) but does have a section about how implementations are allowed to impose limits. C 标准定义了一种具有无限堆栈空间(以及其他资源)的语言,但确实有一部分是关于如何允许实现施加限制的。

Usually it's the operating system that actually first creates the error.通常,实际上首先创建错误的是操作系统。 The OS doesn't care about how many calls you make, but about the total size of the stack.操作系统不关心您进行了多少次调用,而是关心堆栈的总大小 The stack is composed of stack frames , one for each function call.堆栈由堆栈帧组成,每个函数调用一个。 Usually a stack frame consists of some combination of the following five things (as an approximation; details can vary a lot between systems):通常,堆栈帧由以下五项的某种组合组成(作为近似值;系统之间的细节可能会有很大差异):

  1. The parameters to the function call (probably not actually here, in this case; they're probably in registers, although this doesn't actually buy anything with recursion).函数调用的参数(在这种情况下可能实际上并不在这里;它们可能在寄存器中,尽管这实际上并没有购买任何递归)。
  2. The return address of the function call (in this case, the address of the ++i instruction in the for loop).函数调用的返回地址(这里是for循环中++i指令的地址)。
  3. The base pointer where the previous stack frame starts前一个堆栈帧开始的基指针
  4. Local variables (at least those that don't go in registers)局部变量(至少那些不在寄存器中的)
  5. Any registers the caller wants to save when it makes a new function call, so the called function doesn't overwrite them (some registers may be saved by the caller instead, but it doesn't particularly matter for stack size analysis).调用者在进行新函数调用时想要保存的任何寄存器,因此被调用的函数不会覆盖它们(某些寄存器可能由调用者保存,但对于堆栈大小分析并不特别重要)。 This is why passing parameters in registers doesn't help much in this case;这就是为什么在这种情况下在寄存器中传递参数没有多大帮助的原因。 they'll end up on the stack sooner or later.他们迟早会进入堆栈。

Because some of these (specifically, 1., 4., and 5.) can vary in size by a lot, it can be difficult to estimate how big an average stack frame is, although it's easier in this case because of the recursion.因为其中一些(特别是 1.、4. 和 5.)的大小可能有很大差异,所以很难估计平均堆栈帧有多大,尽管在这种情况下由于递归会更容易。 Different systems also have different stack sizes;不同的系统也有不同的堆栈大小; it currently looks like by default I can have 8 MiB for a stack, but an embedded system would probably have a lot less.目前看起来默认情况下我可以有 8 MiB 用于堆栈,但嵌入式系统可能会少很多。

This also explains why removing a local variable gives you more available function calls;这也解释了为什么删除局部变量会为您提供更多可用的函数调用; you reduced the size of each of the 500,000 stack frames.您减少了 500,000 个堆栈帧中的每一个的大小。


If you want to increase the amount of stack space available, look into the setrlimit(2) function (on Linux like the OP; it may be different on other systems).如果您想增加可用的堆栈空间量,请查看setrlimit(2)函数(在 Linux 上类似于 OP;在其他系统上可能会有所不同)。 First, though, you might want to try debugging and refactoring to make sure you need all that stack space.不过,首先,您可能想尝试调试和重构以确保您需要所有堆栈空间。

For each time a function recurs, your program takes more memory on the stack, the memory it takes for each function depends upon the function and variables within it.每次函数重复出现时,您的程序都会在堆栈上占用更多内存,每个函数占用的内存取决于函数和其中的变量。 The number of recursions that can be done of a function is entirely dependant upon your system.一个函数可以完成的递归次数完全取决于您的系统。

There is no general number of recursions that will cause stack overflow.没有一般的递归次数会导致堆栈溢出。

Removing the variable 'neighbour' will allow for the function to recur further as each recursion takes less memory, but it will still eventually cause stack overflow.删除变量“邻居”将允许函数进一步递归,因为每次递归占用的内存更少,但最终仍会导致堆栈溢出。

This is a simple c# function that will show you how many iteration your computer can take before stack overflow (as a reference, I have run up to 10478):这是一个简单的 c# 函数,它将显示您的计算机在堆栈溢出之前可以进行多少次迭代(作为参考,我已运行到 10478):

    private void button3_Click(object sender, EventArgs e)
    {
        Int32 lngMax = 0;
        StackIt(ref lngMax);
    }

    private void StackIt(ref Int32 plngMax, Int32 plngStack = 0)
    {
        if (plngStack > plngMax)
        {
            plngMax = plngStack;
            Console.WriteLine(plngMax.ToString());
        }

        plngStack++;
        StackIt(ref plngMax, plngStack);
    }

in this simple case, the condition check: "if (plngStack > plngMax)" could be removed, but if you got a real recursive function, this check will help you localize the problem.在这个简单的例子中,条件检查:“if (plngStack > plngMax)”可以被删除,但是如果你有一个真正的递归函数,这个检查将帮助你定位问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM