web-dev-qa-db-ja.com

Javaの深い再帰からスタックがオーバーフローしますか?

関数型言語のいくつかの経験の後、Java-で再帰を使用し始めていますが、言語には約1000の比較的浅い呼び出しスタックがあるようです。

呼び出しスタックを大きくする方法はありますか? Erlangのように、何百万回もの呼び出しを行う関数を作成できますか?

Project Eulerの問題を起こすとき、私はこれにますます気づいています。

ありがとう。

51
Lucky

これらのパラメーターを使用できると思います

-ss Stacksizeでネイティブスタックサイズを増やすか、

-oss Stacksize Javaスタックサイズを増やし、

デフォルトのネイティブスタックサイズは128kで、最小値は1000バイトです。デフォルトのJavaスタックサイズは400kで、最小値は1000バイトです。

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

編集:

最初のコメント(チャック)を読んだ後、質問を読み直して別の回答を読んだ後、質問を「スタックサイズを増やす」と解釈したことを明確にしたいと思います。関数型プログラミング(その表面をかき傷しただけのプログラミングパラダイム)のように、無限のスタックを持つことができると言うつもりはありませんでした。

40
Tom

スタックサイズを大きくすると、一時的な包帯としてのみ機能します。他の人が指摘したように、本当に必要なのは末尾呼び出しの削除であり、Javaにはさまざまな理由でこれはありません。しかし、必要に応じてチートできます。

手に赤い丸薬? OK、このようにしてください。

スタックをヒープに交換する方法はいくつかあります。たとえば、関数内で再帰呼び出しを行う代わりに、評価時に呼び出しを行う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()が呼び出されたときにストリームの残りを返すサンクのようなものであることに注意してください。確かに再帰のように見えますが、mapの再帰呼び出しは行われず、Streamデータ構造の一部になります。

これは通常のfor-constructで巻き戻すことができます。

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

あなたがプロジェクトオイラーについて話していたので、ここに別の例があります。このプログラムは相互に再帰的な関数を使用し、何百万回の呼び出しでもスタックを爆破しません。

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())));}}

スタックをヒープに交換するためにできるもう1つのことは、複数のスレッドを使用することです。アイデアは、再帰呼び出しを行う代わりに、呼び出しを行うサンクを作成し、このサンクを新しいスレッドに渡し、現在のスレッドに関数を終了させることです。これStackless Pythonなどの背後にある考え方です。

以下は、Javaでの例です。 import static句なしで見るのは少し不透明であることをおologiesびします:

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関数はサンクをスレッドプールに渡し、Promiseを返します。これはJava.util.concurrent.Futureにのみ似ていますが、より良い。 こちらをご覧ください ポイントは、上記のメソッドが右再帰データ構造をO(1) stack、通常はテールコールの除去が必要ですので、複雑さの代わりにTCEを効果的に達成しました。この関数は次のように呼び出します。

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

この後者の手法は、非線形再帰に対して完璧に機能することに注意してください。つまり、末尾呼び出しのないアルゴリズムであっても、一定のスタックで実行されます。

もう1つできることは、trampoliningと呼ばれる手法を採用することです。トランポリンは、ステップスルーできるデータ構造として具体化された計算です。 Functional Java library には、私が書いた Trampoline データ型が含まれています。例として ここでは定数スタックで右に折りたたむトランポリンfoldRightCがあります

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を使用した単一ループ。

86
Apocalisp

末尾再帰を使用するかどうかはJVMに依存します-それらのいずれかが実行されるかどうかはわからないのですが、それに依存するべきではありません。具体的には、実際に使用する再帰レベルの数に厳しい制限がなく、それぞれのスタックスペースの正確な量を知っている場合を除き、スタックサイズを変更するとveryになることはめったにありませんかかります。とても壊れやすい。

基本的に、ビルドされていない言語では無制限の再帰を使用しないでください。代わりに反復を使用する必要があります、私は恐れています。そして、はい、それは時々わずかな痛みになる可能性があります:(

23
Jon Skeet

質問する必要がある場合、おそらく何か間違ったことをしている

おそらく、Javaのデフォルトスタックを増やす方法を見つけることができますが、増加したスタックに頼るのではなく、やりたいことを行う別の方法を本当に見つける必要があるという点で、2セントを追加します。

Java仕様は、JVMが末尾再帰最適化手法を実装することを必須にしないため、問題を回避する唯一の方法は、スタックプレッシャーを減らすことです。追跡する必要があるローカル変数/パラメータ、または理想的には再帰レベルを大幅に下げるか、またはまったく再帰せずに書き換えるのが理想的です。

ほとんどの関数型言語は、末尾再帰をサポートしています。ただし、ほとんどのJavaコンパイラはこれをサポートしていません。代わりに別の関数呼び出しを行います。つまり、実行できる再帰呼び出しの数には常に上限があります。最終的にはスタックスペースが不足します)。

末尾再帰では、再帰している関数のスタックフレームを再利用するため、スタックに同じ制約はありません。

8
Sean

これはコマンドラインで設定できます:

Java -Xss8Mクラス

7
stili

Java VMで実行されるClojureは、末尾呼び出しの最適化を実装することを非常に望んでいますが、JVMバイトコードの制限のためにできません(詳細はわかりません)結果として、適切な末尾再帰から期待されるいくつかの基本的な機能を実装する特別な「再帰」形式でのみそれ自身を助けることができます。

とにかく、これは現在JVMがcannotテールコール最適化をサポートしていることを意味します。 JVMの一般的なループ構造として再帰を使用しないことを強くお勧めします。私の個人的な見解では、Javaは十分なレベルの言語ではありません。

6
Svante
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())));
}
1
test

私は同じ問題にぶつかり、最終的にfor-loopに再帰を書き換えるになり、それでうまくいきました。

0
Renaud