【问题标题】:how many recursive function calls causes stack overflow?多少递归函数调用导致堆栈溢出?
【发布时间】:2018-06-19 02:31:12
【问题描述】:

我正在研究一个用 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);
        }
    }
}

【问题讨论】:

  • 多少?直到堆栈用完。在 Windows 下,系统可以添加额外的内存,所以......这可能需要很多。在你的情况下,你没有很多局部变量(只有一个指针),所以它只是这个指针和函数框架 - 真的太少了。
  • 在CentOs 7上500000次递归调用后出现堆栈溢出是否正常?
  • 这看起来 awfully familiar ... 500k 调用就可以了,但这完全取决于你系统的堆栈大小和你在那里推送的内容,看看 here
  • 如果您需要超过 100 级的递归,那么递归并不是解决问题的正确方法(不包括大多数体面的编译器会为您转换为迭代的“尾”递归)。
  • Linux 机器上的默认堆栈大小为 8 MiB。如果每次调用需要超过 16 个字节的数据(包括返回地址等),那么在 500,000 次调用深度时会遇到问题(但如果您已经从 499,900 个调用中返回,则不会在 500,000 次调用时遇到问题)。引起麻烦的是递归调用的级别数 - 堆栈溢出 - 而不是调用的总数。

标签: c recursion stack-overflow


【解决方案1】:

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

在这个简单的例子中,条件检查:“if (plngStack > plngMax)”可以被删除, 但如果你有一个真正的递归函数,这个检查将帮助你定位问题。

【讨论】:

    【解决方案2】:
    1. 是和否 - 如果您在代码中遇到堆栈溢出,这可能意味着一些事情

      • 您的算法没有以尊重您获得的堆栈上的内存量的方式实现。您可以调整此数量以适应算法的需要。

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

      • 这是一个试图吃掉所有 RAM 的错误。您忘记了递归中的基本情况或错误地调用了相同的函数。我们都做到了至少 2 次。

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

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

    我建议您详细了解堆栈如何为您工作。 Here's a good resource 在软件工程堆栈交换上。还有一个在stack overflow 上(zing!)

    【讨论】:

    • 谢谢你的回答,我的代码很好,但是我使用的算法必须用递归函数实现,对于小的递归深度我没有问题,我的结果很好。
    • 由于堆栈大小在大多数环境中是可设置的值,因此您对 1 的回答并不令人信服。即堆栈溢出不一定意味着错误代码。这只是意味着需要针对该特定应用程序调整堆栈大小。
    • @ryyker 我可以看到这种解释。我会稍微更新一下措辞。
    • 关于 1,系统通常会限制堆栈大小超过实际需要。您可能有一个完全好的算法,只需要递归,并且将其重新实现为在堆上具有堆栈结构的循环将使其能够处理大量数据。
    • 是的,你回答的内容很好,但是你回答下的cmets很壮观:)
    【解决方案3】:

    我想知道这正常吗?

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

    在下面的代码中,去掉局部变量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
    

    【讨论】:

    • 注意:我的结果是-g。使用-O3 我无法让堆栈溢出。我怀疑编译器已经优化了我的简单递归。
    【解决方案4】:

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

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

    1. 函数调用的参数(在这种情况下实际上可能不在此处;它们可能在寄存器中,尽管这实际上并没有通过递归购买任何东西)。
    2. 函数调用的返回地址(在本例中为for 循环中++i 指令的地址)。
    3. 上一个堆栈帧开始的基指针
    4. 局部变量(至少那些不进入寄存器的变量)
    5. 调用者在进行新函数调用时要保存的任何寄存器,因此被调用函数不会覆盖它们(某些寄存器可能由调用者保存,但对于堆栈大小分析并不特别重要) .这就是为什么在这种情况下在寄存器中传递参数没有多大帮助的原因;它们迟早会进入堆栈。

    因为其中一些(特别是 1.、4. 和 5.)的大小可能相差很大,所以很难估计平均堆栈帧有多大,尽管在这种情况下更容易,因为递归。不同的系统也有不同的堆栈大小;目前看起来默认情况下我可以拥有 8 MiB 的堆栈,但嵌入式系统可能会少很多。

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


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

    【讨论】:

    • 这写得很好,但它没有回答 OP 问题中编号的问题,也没有解释其中固有的错误概念。例如,Q1:是的,许多调用溢出堆栈是正常的。 Q2:溢出堆栈所需的调用次数取决于每个函数需要多少空间。 Q3:删除neighbor 可以减少一次调用占用的空间,增加溢出前可以调用的次数,但不会阻止。这些都没有在这个答案中直接解决。
    • @EricPostpischil 我没有很好地编号,但我确实解决了所有问题。相应地,答案是“大量调用压倒堆栈是正常的,但我不知道 500,000 在你的环境中是否是一个合理的数字”,“重要的不是调用数量而是帧大小”,和“删除 neighbor 会减小帧大小,因此会增加溢出所需的调用次数”。
    【解决方案5】:

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

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

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

    【讨论】:

    • 我的系统是 Centos 7,RAM 4 GB,拥有者核心 i7,500000 次递归调用足以在这个系统中堆栈溢出?
    • @mehrdad - 你为什么不试试看呢?
    • @mehrdad 检查ulimit -s.
    猜你喜欢
    • 2011-02-26
    • 2013-04-05
    • 2020-12-17
    • 2020-03-06
    • 1970-01-01
    • 2012-08-27
    相关资源
    最近更新 更多