簡體   English   中英

有多少遞歸函數調用會導致堆棧溢出?

[英]how many recursive function calls causes 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);
        }
    }
}

我想知道這正常嗎?

是的。 只有這么多的堆棧大小。

在下面的代碼中,刪除局部變量鄰居可以防止堆棧溢出嗎?

不。即使沒有變量和返回值,函數調用本身也必須存儲在堆棧中,以便最終可以展開堆棧。

例如...

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. 是與否 - 如果您在代碼中遇到堆棧溢出,這可能意味着一些事情

    • 您的算法沒有以尊重您獲得的堆棧上的內存量的方式實現。 您可以調整此數量以滿足算法的需要。

      如果是這種情況,更常見的是更改算法以更有效地利用堆棧,而不是添加更多內存。 例如,將遞歸函數轉換為迭代函數可以節省大量寶貴的內存。

    • 這是一個試圖吃掉你所有內存的錯誤。 您忘記了遞歸中的基本情況或錯誤地調用了相同的函數。 我們都至少做過2次。

  2. 不一定有多少調用會導致溢出 - 它取決於每個單獨調用在堆棧幀上占用多少內存。 每個函數調用都用完堆棧內存,直到調用返回。 堆棧內存是靜態分配的——你不能在運行時改變它(在一個理智的世界里)。 這是幕后的后進先出 (LIFO) 數據結構。

  3. 它並沒有阻止它,它只是改變了溢出堆棧內存所需的對grow_Wolff_cluster調用grow_Wolff_cluster 在 32 位系統上,從函數中刪除neighbor的調用成本grow_Wolff_cluster 4 個字節。 當您將其乘以數十萬時,它會迅速加起來。

我建議您了解更多有關堆棧如何為您工作的信息。 這是關於軟件工程堆棧交換的一個很好的資源 另一個堆棧溢出(zing!)

堆棧溢出不是由 C 標准定義的,而是由實現定義的。 C 標准定義了一種具有無限堆棧空間(以及其他資源)的語言,但確實有一部分是關於如何允許實現施加限制的。

通常,實際上首先創建錯誤的是操作系統。 操作系統不關心您進行了多少次調用,而是關心堆棧的總大小 堆棧由堆棧幀組成,每個函數調用一個。 通常,堆棧幀由以下五項的某種組合組成(作為近似值;系統之間的細節可能會有很大差異):

  1. 函數調用的參數(在這種情況下可能實際上並不在這里;它們可能在寄存器中,盡管這實際上並沒有購買任何遞歸)。
  2. 函數調用的返回地址(這里是for循環中++i指令的地址)。
  3. 前一個堆棧幀開始的基指針
  4. 局部變量(至少那些不在寄存器中的)
  5. 調用者在進行新函數調用時想要保存的任何寄存器,因此被調用的函數不會覆蓋它們(某些寄存器可能由調用者保存,但對於堆棧大小分析並不特別重要)。 這就是為什么在這種情況下在寄存器中傳遞參數沒有多大幫助的原因。 他們遲早會進入堆棧。

因為其中一些(特別是 1.、4. 和 5.)的大小可能有很大差異,所以很難估計平均堆棧幀有多大,盡管在這種情況下由於遞歸會更容易。 不同的系統也有不同的堆棧大小; 目前看起來默認情況下我可以有 8 MiB 用於堆棧,但嵌入式系統可能會少很多。

這也解釋了為什么刪除局部變量會為您提供更多可用的函數調用; 您減少了 500,000 個堆棧幀中的每一個的大小。


如果您想增加可用的堆棧空間量,請查看setrlimit(2)函數(在 Linux 上類似於 OP;在其他系統上可能會有所不同)。 不過,首先,您可能想嘗試調試和重構以確保您需要所有堆棧空間。

每次函數重復出現時,您的程序都會在堆棧上占用更多內存,每個函數占用的內存取決於函數和其中的變量。 一個函數可以完成的遞歸次數完全取決於您的系統。

沒有一般的遞歸次數會導致堆棧溢出。

刪除變量“鄰居”將允許函數進一步遞歸,因為每次遞歸占用的內存更少,但最終仍會導致堆棧溢出。

這是一個簡單的 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)”可以被刪除,但是如果你有一個真正的遞歸函數,這個檢查將幫助你定位問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM