繁体   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