簡體   English   中英

隨機遞歸方法中Math.Random中的StackOverflowError

[英]StackOverflowError in Math.Random in a randomly recursive method

這是我程序的上下文。

函數有50%的機會不執行任何操作,有50%的機會兩次調用自身。 程序完成的概率是多少?

我寫了這段代碼,顯然效果很好。 對於每個人來說可能不是很明顯的答案是該程序有100%的機會完成。 但是,當我運行此程序時,在Math.Random()中出現了StackOverflowError(多么方便;)。 有人可以指出它的來源,並告訴我我的代碼是否錯誤?

static int bestDepth =0;
static int numberOfPrograms =0;
@Test
public void testProba(){
   for(int i = 0; i <1000; i++){
       long time = System.currentTimeMillis();
       bestDepth = 0;
       numberOfPrograms = 0;
       loop(0);
       LOGGER.info("Best depth:"+ bestDepth +" in "+(System.currentTimeMillis()-time)+"ms");
   }
}

public boolean loop(int depth){
    numberOfPrograms++;
    if(depth> bestDepth){
        bestDepth = depth;
    }
    if(proba()){
        return true;
    }
    else{
        return loop(depth + 1) && loop(depth + 1);
    }
}

public boolean proba(){
    return Math.random()>0.5;
}

java.lang.StackOverflowError
at java.util.Random.nextDouble(Random.java:394)
at java.lang.Math.random(Math.java:695)

我懷疑堆棧及其中的功能數量有限,但是我在這里看不到問題。

任何建議或線索顯然都是受歡迎的。

法比恩

編輯:謝謝您的回答,我用java -Xss4m運行了它,效果很好。

每當調用函數或創建非靜態變量時,都將使用堆棧來為其放置和保留空間。

現在,似乎您正在遞歸地調用loop函數。 這會將參數以及代碼段和返回地址放入堆棧中。 這意味着很多信息都放在堆棧上。

但是,堆棧是有限的。 CPU具有內置機制,可防止將數據壓入堆棧並最終覆蓋代碼本身(隨着堆棧變小)的問題。 這稱為General Protection Fault 當發生一般性保護故障時,操作系統會通知當前正在運行的任務。 因此,發起了Stackoverflow

這似乎發生在Math.random()

為了解決您的問題,建議您使用Java的-Xss選項增加堆棧大小。

如您所說, loop函數遞歸調用自身。 現在,編譯器可以將尾部遞歸調用重寫為循環,而不會占用任何堆棧空間(這稱為尾部調用優化TCO)。 不幸的是,java編譯器無法做到這一點。 而且您的loop也不是尾遞歸的。 您的選擇如下:

  1. 根據其他答案的建議,增加堆棧大小。 請注意,這只會及時推遲問題的發生:無論堆棧多大,堆棧的大小仍然是有限的。 您只需要更長的遞歸調用鏈即可突破空間限制。
  2. 根據循環重寫函數
  3. 使用一種語言 ,該語言具有執行TCO的編譯器
    1. 您仍然需要將函數重寫為尾遞歸
    2. 或用蹦床將其重寫(僅需進行較小的更改)。 一篇解釋蹦床並進一步進行概括的好論文稱為“ 帶有免費Monad的Stackless Scala ”。

為了說明3.2中的觀點,下面是重寫后的函數的樣子:

def loop(depth: Int): Trampoline[Boolean] = {
  numberOfPrograms = numberOfPrograms + 1
  if(depth > bestDepth) {
    bestDepth = depth
  }
  if(proba()) done(true)
  else for {
    r1 <- loop(depth + 1)
    r2 <- loop(depth + 1)
  } yield r1 && r2
}

最初的調用是loop(0).run

增加堆棧大小是一個不錯的臨時解決方法。 然而,正如證明了這個帖子 ,雖然loop()函數確保最終能夠返回,所要求的平均籌碼深度loop()無限的 因此,無論增加多少堆棧,程序最終都會耗盡內存並崩潰。

我們無法采取任何措施來防止這種情況的發生; 我們總是需要以某種方式在內存中對堆棧進行編碼,而我們永遠不會擁有無限的內存。 但是,有一種方法可以將您使用的內存量減少大約2個數量級。 這應該給你的程序返回,而不是崩潰的顯著機會較高。

我們可以通過注意到堆棧的每一層來執行此操作,實際上只需要一條信息即可運行您的程序:該條信息告訴我們返回后是否需要再次調用loop() 因此,我們可以使用一堆位來模擬遞歸。 每個模擬的堆棧幀將只需要一個內存 (現在,它需要的內存量是它的64-96倍,具體取決於您是以32位還是64位運行)

代碼看起來像這樣(盡管我現在沒有Java編譯器,所以我無法對其進行測試)

static int bestDepth = 0;
static int numLoopCalls = 0;

public void emulateLoop() {
    //Our fake stack.  We'll push a 1 when this point on the stack needs a second call to loop() made yet, a 0 if it doesn't
    BitSet fakeStack = new BitSet();
    long currentDepth = 0;
    numLoopCalls = 0;

    while(currentDepth >= 0)
    {
        numLoopCalls++;

        if(proba()) {
            //"return" from the current function, going up the callstack until we hit a point that we need to "call loop()"" a second time
            fakeStack.clear(currentDepth);
            while(!fakeStack.get(currentDepth))
            {
                currentDepth--;
                if(currentDepth < 0)
                {
                    return;
                }
            }

            //At this point, we've hit a point where loop() needs to be called a second time.
            //Mark it as called, and call it
            fakeStack.clear(currentDepth);
            currentDepth++;
        }
        else {
            //Need to call loop() twice, so we push a 1 and continue the while-loop
            fakeStack.set(currentDepth);
            currentDepth++;
            if(currentDepth > bestDepth)
            {
                bestDepth = currentDepth;
            }
        }
    }
}

這可能會稍慢一些,但會占用大約1/100的內存。 請注意, BitSet存儲在堆中,因此不再需要增加堆棧大小來運行它。 如果有的話,您將需要增加heap-size

遞歸的缺點是它開始填滿堆棧,如果遞歸太深,最終將導致堆棧溢出。 如果要確保測試結束,可以使用以下Stackoverflow線程中給出的答案來增加堆棧大小:

如何增加Java堆棧大小?

暫無
暫無

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

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