堆栈是否从Java中的深度递归中溢出?

时间:2009-05-13 21:35:45

标签: java functional-programming stack overflow

在使用函数式语言之后,我开始在Java中使用更多的递归 - 但是语言似乎有一个相对较浅的调用堆栈,大约1000个。

有没有办法让调用堆栈更大?我可以像在Erlang中那样创建数百万次调用的函数吗?

当我做Project Euler问题时,我越来越注意到这一点。

感谢。

10 个答案:

答案 0 :(得分:86)

增加堆叠大小只能作为临时绷带。正如其他人所指出的那样,你真正想要的是尾部调用消除,而Java由于各种原因没有这个。但是,如果你愿意,你可以作弊。

手上有红丸吗?好的,请这样。

有些方法可以为堆交换堆栈。例如,不是在函数内进行递归调用,而是让它返回一个 lazy datastructure ,在评估时调用它。然后,您可以使用Java的for-construct来展开“堆栈”。我将用一个例子来证明。考虑一下这个Haskell代码:

map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = (f x) : map f xs

请注意,此函数从不评估列表的尾部。因此该函数实际上不需要进行递归调用。在Haskell中,它实际上为尾部返回一个 thunk ,如果需要它就会被调用。我们可以在Java中做同样的事情(这使用来自Functional Java的类):

public <B> Stream<B> map(final F<A, B> f, final Stream<A> as)
  {return as.isEmpty()
     ? nil()
     : cons(f.f(as.head()), new P1<Stream<A>>()
         {public Stream<A> _1()
           {return map(f, as.tail);}});}

请注意,Stream<A>由类型A的值和类型P1的值组成,类似于在调用_1()时返回流的其余部分的thunk。虽然它看起来像递归,但是没有对map进行递归调用,而是成为Stream数据结构的一部分。

然后可以使用常规for-construct解开。

for (Stream<B> b = bs; b.isNotEmpty(); b = b.tail()._1())
  {System.out.println(b.head());}

这是另一个例子,因为你在谈论Project Euler。该程序使用相互递归的函数,并且不会破坏堆栈,即使是数百万次调用:

import fj.*; import fj.data.Natural;
import static fj.data.Enumerator.naturalEnumerator;
import static fj.data.Natural.*; import static fj.pre.Ord.naturalOrd;
import fj.data.Stream; import fj.data.vector.V2;
import static fj.data.Stream.*; import static fj.pre.Show.*;

public class Primes
  {public static Stream<Natural> primes()
    {return cons(natural(2).some(), new P1<Stream<Natural>>()
       {public Stream<Natural> _1()
         {return forever(naturalEnumerator, natural(3).some(), 2)
                 .filter(new F<Natural, Boolean>()
                   {public Boolean f(final Natural n)
                      {return primeFactors(n).length() == 1;}});}});}

   public static Stream<Natural> primeFactors(final Natural n)
     {return factor(n, natural(2).some(), primes().tail());}

   public static Stream<Natural> factor(final Natural n, final Natural p,
                                        final P1<Stream<Natural>> ps)
     {for (Stream<Natural> ns = cons(p, ps); true; ns = ns.tail()._1())
          {final Natural h = ns.head();
           final P1<Stream<Natural>> t = ns.tail();
           if (naturalOrd.isGreaterThan(h.multiply(h), n))
              return single(n);
           else {final V2<Natural> dm = n.divmod(h);
                 if (naturalOrd.eq(dm._2(), ZERO))
                    return cons(h, new P1<Stream<Natural>>()
                      {public Stream<Natural> _1()
                        {return factor(dm._1(), h, t);}});}}}

   public static void main(final String[] a)
     {streamShow(naturalShow).println(primes().takeWhile
       (naturalOrd.isLessThan(natural(Long.valueOf(a[0])).some())));}}

为堆栈交换堆栈可以做的另一件事是使用多线程。我的想法是,不是进行递归调用,而是创建一个进行调用的thunk,将这个thunk关闭到一个新线程并让当前线程退出该函数。这是背后的想法像Stackless Python。

以下是Java中的示例。抱歉在没有import static子句的情况下看起来有点不透明:

public static <A, B> Promise<B> foldRight(final Strategy<Unit> s,
                                          final F<A, F<B, B>> f,
                                          final B b,
                                          final List<A> as)
  {return as.isEmpty()
     ? promise(s, P.p(b))
     : liftM2(f).f
         (promise(s, P.p(as.head()))).f
         (join(s, new P1<Promise<B>>>()
            {public Promise<B> _1()
              {return foldRight(s, f, b, as.tail());}}));}

Strategy<Unit> s由线程池支持,promise函数将thunk交给线程池,返回Promise,这非常像java.util.concurrent.Future,只有更好。 See here.关键是上面的方法将右递归数据结构折叠到O(1)堆栈中的右侧,这通常需要消除尾部调用。因此,我们有效地实现了TCE,以换取一些复杂性。您可以按如下方式调用此函数:

Strategy<Unit> s = Strategy.simpleThreadStrategy();
int x = foldRight(s, Integers.add, List.nil(), range(1, 10000)).claim();
System.out.println(x); // 49995000

请注意,后一种技术非常适用于非线性递归。也就是说,它将以常量堆栈运行,即使是没有尾调用的算法。

您可以做的另一件事是使用一种名为 trampolining 的技术。蹦床是一种计算,具体化为数据结构,可以逐步实现。 Functional Java library包含我编写的Trampoline数据类型,它有效地允许您将任何函数调用转换为尾调用。例如here is a trampolined foldRightC that folds to the right in constant stack:

public final <B> Trampoline<B> foldRightC(final F2<A, B, B> f, final B b)
  {return Trampoline.suspend(new P1<Trampoline<B>>()
    {public Trampoline<B> _1()
      {return isEmpty()
         ? Trampoline.pure(b)
         : tail().foldRightC(f, b).map(f.f(head()));}});}

这与使用多个线程的原理相同,除了不是在自己的线程中调用每个步骤,我们在堆上构造每个步骤,非常类似于使用Stream,然后我们运行所有步骤在Trampoline.run的单个循环中。

答案 1 :(得分:40)

我猜你可以使用这些参数

  

-ss Stacksize增加原生   堆栈大小或

     

-oss Stacksize增加Java   堆栈大小,

     

默认的本机堆栈大小为128k,   最小值为1000字节。   默认的Java堆栈大小是400k,   最小值为1000字节。

http://edocs.bea.com/wls/docs61/faq/java.html#251197

编辑:

在阅读了第一条评论(Chuck's)之后,以及重新阅读问题和阅读其他答案时,我想澄清一下,我将这个问题解释为“增加堆栈大小”。我不打算说你可以拥有无​​限的堆栈,例如在函数式编程中(一种我只是表面上看的编程范式)。

答案 2 :(得分:23)

由JVM决定是否使用尾递归 - 我不知道它们是否有任何影响,但你不应该依赖它。特别是,更改堆栈大小非常很少是正确的做法,除非你对实际使用的递归级别有一些硬限制,并且你确切知道每个堆栈空间的确切数量会占用。非常脆弱。

基本上,您不应该在不为其构建的语言中使用无界递归。你不得不使用迭代,我害怕。是的,有时可能会有轻微的痛苦:(

答案 3 :(得分:9)

If you have to ask, you're probably doing something wrong

现在,虽然你可能找到一种方法来增加java中的默认堆栈,但是我只需要加上我的2美分,你真的需要找到另一种方法去做你想做的事情,而不是依赖于增加叠加。

由于java规范没有强制JVM实现尾递归优化技术,解决问题的唯一方法是通过减少需要的局部变量/参数的数量来减少堆栈压力跟踪,或理想情况下只是通过显着降低递归水平,或者根本不重写而不重写。

答案 4 :(得分:8)

大多数函数式语言都支持尾递归。但是,大多数Java编译器都不支持这一点。而是进行另一个函数调用。这意味着你可以使用递归调用的数量总是有一个上限(因为你最终会耗尽堆栈空间)。

使用尾递归,您可以重用递归函数的堆栈帧,因此您对堆栈没有相同的约束。

答案 5 :(得分:7)

您可以在命令行上设置:

java -Xss8M class

答案 6 :(得分:6)

在Java VM上运行的Clojure非常希望实现尾调用优化,但它不能由于JVM字节码的限制(我不知道细节)。因此,它只能通过一种特殊的“复现”形式来帮助自己,它实现了一些你期望从正确的尾递归中获得的基本功能。

无论如何,这意味着当前的JVM无法支持尾调用优化。我强烈建议不要将递归用作JVM上的通用循环结构。我个人认为Java不是一种足够高级的语言。

答案 7 :(得分:1)

public static <A, B> Promise<B> foldRight(final Strategy<Unit> s,
                                          final F<A, F<B, B>> f,
                                          final B b,
                                          final List<A> as)
{
    return as.isEmpty() ? promise(s, P.p(b))
    : liftM2(f).f(promise(s, P.p(as.head())))
      .f(join(s, new F<List<A>, P1<Promise<B>>>()
        {
             public Promise<B> f(List<A> l)
             {
                 return foldRight(s, f, b, l);
             }
         }.f(as.tail())));
}

答案 8 :(得分:0)

我遇到了同样的问题,最后将递归重写为for循环,这就行了。

答案 9 :(得分:-1)

如果你正在使用eclipse中的

,请将 -xss2m 设置为vm参数。

-xss2m直接在命令行上。

java -xss2m classname
相关问题