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

时间:2018-01-10 00:27:17

标签: c recursion stack-overflow

我正在研究用c编写的模拟问题,我程序的主要部分是递归函数。 当递归深度达到大约500000时,似乎发生堆栈溢出。

Q1 :我想知道这是正常的吗?

Q2 :一般来说有多少递归函数调用导致堆栈溢出?

Q3 :在下面的代码中,删除局部变量neighbor可以防止堆栈溢出?

我的代码:

/*
 * 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);
        }
    }
}

5 个答案:

答案 0 :(得分:4)

  

我想知道这是正常的吗?

是。堆栈大小只有这么多。

  

在下面的代码中,删除局部变量neighbor可以防止堆栈溢出?

没有。即使没有变量也没有返回值,函数调用本身必须存储在堆栈中,这样堆栈最终可以解开。

例如......

void recurse() {
    recurse();
}

int main (void)
{
    recurse();
}

这仍然会溢出堆栈。

$ ./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
  

一般来说,有多少递归函数调用导致堆栈溢出?

这取决于您的环境和函数调用。在OS X 10.13上,我默认限制为8192K。

$ ulimit -s
8192

这个带clang -g的简单示例可以递归261976次。使用-O3我无法让它溢出,我怀疑编译器优化已经消除了我的简单递归。

#include <stdio.h>

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

int main (void)
{
    recurse();
}

添加一个整数参数,它是261933次。

#include <stdio.h>

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

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

添加一个双参数,现在是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);
}

添加一些堆栈变量,它是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);
}

等等。但是我可以在这个shell中增加我的堆栈大小并获得两次调用。

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

我有一个很大的上限,我可以制作65,532K或64M的叠加量。

$ ulimit -Hs
65532

答案 1 :(得分:1)

  1. 是和否 - 如果您在代码中遇到堆栈溢出,则可能意味着一些事情

    • 您的算法的实现方式不是考虑到您给定的堆栈内存量。您可以调整此数量以满足算法的需要。

      如果是这种情况,那么更常见的可以更改算法以更有效地利用堆栈,而不是添加更多内存。例如,将递归函数转换为迭代函数可以节省大量宝贵的内存。

    • 尝试吃掉所有内存的错误。你忘记了递归中的基本情况或错误地调用了相同的函数。我们都是在至少 2次完成的。

  2. 不一定多少次调用会导致溢出 - 它取决于每个单独调用占用堆栈帧的内存量。每个函数调用都会占用堆栈内存,直到调用返回为止。堆栈内存是静态分配的 - 您无法在运行时(在一个理智的世界中)更改它。它是幕后的后进先出(LIFO)数据结构。

  3. 它没有阻止它,它只是改变了溢出堆栈内存所需的grow_Wolff_cluster次调用次数。在32位系统上,从函数中删除neighbor会导致调用grow_Wolff_cluster减少4个字节。当你将其乘以数十万时,它会迅速增加。

  4. 我建议您了解有关堆栈如何为您工作的更多信息。 Here's a good resource在软件工程堆栈交换上。另一个在stack overflow(zing!)

答案 2 :(得分:1)

堆栈溢出不是由C标准定义的,而是由实现定义的。 C标准定义了一种具有无限堆栈空间的语言(以及其他资源),但确实有一个关于如何允许实现限制的部分。

通常情况下,操作系统实际上首先会创建错误。操作系统不关心您进行了多少次调用,而是关注堆栈的总大小。堆栈由堆栈帧组成,每个函数调用一个。通常,堆栈帧由以下五个事物的某种组合组成(作为近似值;系统之间的细节可能有很大差异):

  1. 函数调用的参数(实际上可能不在这里,在这种情况下;它们可能在寄存器中,虽然这实际上不会通过递归购买任何东西)。
  2. 函数调用的返回地址(在本例中为++i循环中for指令的地址)。
  3. 前一个堆栈帧启动的基本指针
  4. 局部变量(至少那些不在寄存器中的变量)
  5. 调用者在进行新函数调用时想要保存的任何寄存器,因此被调用的函数不会覆盖它们(调用者可能会保存一些寄存器,但对于堆栈大小分析并不特别重要) 。这就是为什么在寄存器中传递参数对这种情况没有多大帮助;他们迟早会在堆栈上结束。
  6. 因为其中一些(特别是,1,4和5)的大小可能会有很大差异,所以很难估计平均堆栈帧的大小,尽管在这种情况下它更容易因为递归。不同的系统也有不同的堆栈大小;它目前看起来像默认情况下我可以有8 MiB的堆栈,但嵌入式系统可能会少很多。

    这也解释了为什么删除局部变量会为您提供更多可用的函数调用;你缩小了500,000个堆栈帧的大小。

    如果你想增加可用的堆栈空间,请查看setrlimit(2) function(在Linux上就像OP一样;在其他系统上可能会有所不同)。首先,您可能希望尝试调试和重构,以确保您需要所有堆栈空间。

答案 3 :(得分:0)

每次函数重复时,程序在堆栈上占用更多内存,每个函数占用的内存取决于函数及其中的变量。可以对函数执行的递归次数完全取决于您的系统。

没有一般的递归数会导致堆栈溢出。

删除变量'neighbor'将允许函数进一步重复,因为每次递归占用的内存较少,但最终仍会导致堆栈溢出。

答案 4 :(得分:0)

这是一个简单的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);
    }

在这种简单情况下,条件检查:“是否可以删除(plngStack> plngMax)”, 但是如果您拥有真正的递归函数,此检查将帮助您定位问题。