[英]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
也不是尾遞歸的。 您的選擇如下:
為了說明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線程中給出的答案來增加堆棧大小:
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.