Java の深い再帰によってスタック オーバーフローが発生しますか?
-
21-08-2019 - |
質問
関数型言語である程度の経験を積んだ後、私は Java で再帰をもっと使い始めています。しかし、この言語の呼び出しスタックは約 1000 と比較的浅いようです。
呼び出しスタックを大きくする方法はありますか?Erlang のように、何百万もの呼び出しを行う関数を作成できるでしょうか?
Project Euler の問題を解くと、このことにますます気づきます。
ありがとう。
解決
私はあなたがこれらのパラメータを使用することができます推測します。
-ssスタックサイズは、ネイティブを高めるために スタックサイズまたは
-ossスタックサイズは、Javaを高めるために スタックサイズ、
デフォルトのネイティブスタックサイズは、128Kであります 1000バイトの最小値を持ちます。 デフォルトのJavaスタックサイズは400Kです、 1000バイトの最小値を有する。
http://edocs.bea.com/wls/docs61/よくある質問/ java.html#251197 の
EDITます:
最初のコメント(Chuck's)だけでなく、再質問を読んで、別の答えを読んを読んだ後、i'd私はちょうど「増加のスタックサイズ」と質問を解釈することを明らかにしたいです。私はあなたがそのような関数型プログラミング(のみその表面に傷i'veプログラミングパラダイム)のように無限のスタックを持つことができると言うつもりdidn'tます。
他のヒント
スタック サイズを増やすことは、一時的な包帯としてのみ機能します。他の人が指摘したように、本当に必要なのは末尾呼び出しの削除ですが、Java にはさまざまな理由からこれがありません。ただし、必要に応じて不正行為を行うこともできます。
手には赤い錠剤?はい、こちらをどうぞ。
スタックをヒープに交換する方法はいくつかあります。たとえば、関数内で再帰呼び出しを行う代わりに、関数が 遅延データ構造 評価時に呼び出しを行います。その後、Java の for-construct を使用して「スタック」を巻き戻すことができます。例を挙げて説明します。次の Haskell コードを考えてみましょう。
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = (f x) : map f xs
この関数はリストの末尾を決して評価しないことに注意してください。したがって、関数は実際に再帰呼び出しを行う必要はありません。Haskell では、実際には サンク 尾部は必要な場合に呼び出されます。同じことを Java でも実行できます (これは次のクラスを使用します) 関数型 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>
type の値で構成されます A
および type の値 P1
これは、_1() が呼び出されたときにストリームの残りを返すサンクのようなものです。確かに再帰のように見えますが、map への再帰呼び出しは行われず、Stream データ構造の一部になります。
これは、通常の for 構造を使用して巻き戻すことができます。
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())));}}
スタックをヒープに交換するためにできるもう 1 つの方法は、次のようにすることです。 複数のスレッド. 。このアイデアは、再帰呼び出しを行う代わりに、 呼び出しを行うサンクを作成し、このサンクを新しいスレッドに渡し、現在のスレッドが関数を終了できるようにします。 これが 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
関数はスレッド プールにサンクを渡し、 Promise
, 、これは非常によく似ています java.util.concurrent.Future
, 、さらに良いです。 ここを参照してください。 ポイントは上記の方法です 右再帰データ構造を 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
この後者の手法は非線形再帰に対して完全にうまく機能することに注意してください。つまり、末尾呼び出しのないアルゴリズムであっても定数スタックで実行されます。
もう 1 つできることは、と呼ばれるテクニックを使用することです。 トランポリン. 。トランポリンは、データ構造として具体化された、ステップ実行できる計算です。の 機能的なJavaライブラリ が含まれています 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
.
これは、末尾再帰を使用するかどうかをJVM次第です - 私はそれらのいずれかを行うかどうかぶっきらぼうわかりませんが、あなたはそれに頼るべきではありません。あなたが実際に使用するどのように多くの再帰のレベルのいくつかのハードの限界を持っていた、とあなたはそれぞれどのくらいのスタック領域を知っていない限り、具体的には、スタックサイズを変更するは非常にの稀に、行うには正しいことないだろう取るでしょう。非常にもろいます。
基本的に、あなたはそれのために構築されていない言語で無限の再帰を使用しないでください。あなたは、私が怖い、代わりに反復を使用する必要があります。そして、はい、それは時々わずかな痛みをすることができます:(
あなたはおそらく、Javaでデフォルトのスタックを高めるための方法を見つけることができますしながら、今、私はちょうどそれに私の2セントを追加してみましょうあなたは本当にあなたがやりたいために別の方法を見つける必要がある、代わりの増加に頼りますスタックます。
Javaの仕様は、それが必須JVMの末尾再帰の最適化技術を実装するために作成しませんので、、問題を回避する唯一の方法は、必要とするローカル変数/パラメータの数を減らすことにより、いずれか、スタックの圧力を減少させることですただ大幅に再帰のレベルを低下させる、あるいは単に全く再帰せずに書き換えることで、あるいは理想的のトラックを保持することにする。
ほとんどの関数型言語は、末尾再帰をサポートしています。しかし、ほとんどのJavaコンパイラはこれをサポートしていません。その代わりに、別の関数呼び出しを行います。これは、(あなたが最終的にスタック領域が不足しますと)常にあなたが作ることができます再帰呼び出しの数に上限があることを意味します。
末尾再帰を使用すると、再帰される関数のスタックフレームを再利用するので、あなたは、スタック上の同じ制約を持っていません。
あなたはコマンドラインでこれを設定することができます:
Javaの-Xss8Mクラス
のJava VM上で動作するClojureのは、非常に多く、末尾呼び出しの最適化を実現したいと思いますが、それはJVMバイトコードの制約に起因することはできません(私は詳細を知りません)。結果として、それはあなたが適切な末尾再帰に期待するいくつかの基本的な機能を実装して、特別な「再発」の形で自分自身を助けることができます。
とにかく、これはJVMが現在を意味のサポート末尾呼び出しの最適化はできません。私は強くJVM上の一般的なループ構造として再帰を使用しないことをお勧めします。私の個人的な見解では、Javaが十分に高いレベルの言語ではないということです。
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())));
}
私は同じ問題に遭遇した、となってしまったの forループをそのやったトリックに再帰を書き換えます。
日食では、設定、使用している場合の -Xss2m のVM引数としてます。
または
コマンドライン上で直接-Xss2mます。
java -xss2m classname