web-dev-qa-db-ja.com

再帰メソッドの最大呼び出し深度を予測するにはどうすればよいですか?

再帰的メソッドが特定のメモリ量で達成できる最大呼び出し深度を見積もる目的で、スタックオーバーフローエラーが発生する前に使用されるメモリを計算するための(おおよその)式は何ですか?

編集:

多くの人が「依存する」と答えましたが、これは合理的です。簡単ですが具体的な例を使用して、いくつかの変数を削除しましょう。

public static int sumOneToN(int n) {
    return n < 2 ? 1 : n + sumOneToN(n - 1);
}

私のEclipseでこれを実行するとIDE nが1000未満で爆発する(私には驚くほど低い)ことを示すのは簡単です)この呼び出し深度制限は実行せずに推定できたでしょうか?

編集:998に到達したため、Eclipseの最大呼び出し深度が1000に固定されていると思わずにはいられませんが、メイン用に1つ、メソッドの最初の呼び出し用に1つあり、1000 全部で。これは偶然ではない数の私見の「丸すぎる」です。さらに調査します。 -XssvmパラメーターにDuxオーバーヘッドがあります。これは最大スタックサイズであるため、Eclipseランナーには-Xss1000をどこかに設定する必要があります

49
Bohemian

これは明らかにJVM固有であり、場合によってはアーキテクチャ固有でもあります。

私は以下を測定しました:

  static int i = 0;
  public static void rec0() {
      i++;
      rec0();
  }

  public static void main(String[] args) {
      ...
      try {
          i = 0; rec0();
      } catch (StackOverflowError e) {
          System.out.println(i);
      }
      ...
  }

を使用して

Java(TM) SE Runtime Environment (build 1.7.0_09-b05)
Java HotSpot(TM) 64-Bit Server VM (build 23.5-b02, mixed mode)

x86で実行されています。

20MBの場合Javaスタック(-Xss20m)、償却コストは1回の呼び出しあたり約16〜17バイト変動しました。私が見た中で最も低いのは16.15バイト/フレームでした。したがって、コストは16バイトで、残りはその他の(固定)オーバーヘッドであると結論付けます。

単一のintをとる関数は、基本的に同じコスト、16バイト/フレームです。

興味深いことに、10個のintsをとる関数には、32バイト/フレームが必要です。なぜこんなに安いのかわかりません。

上記の結果は、コードがJITコンパイルされた後に適用されます。コンパイル前は、フレームあたりのコストははるかに高く、高くなっています。確実に見積もる方法はまだわかりません。 ただし、これは、再帰関数がJITコンパイルされているかどうかを確実に予測できるようになるまで、最大再帰深度を確実に予測する見込みがないことを意味します。

これらはすべて、128Kおよび8MBのulimitスタックサイズでテストされました。結果はどちらの場合も同じでした。

25
NPE

部分的な答えのみ: JVM仕様7、2.5.2 から、スタックフレームをヒープに割り当てることができ、スタックサイズは動的である可能性があります。はっきりとは言えませんが、スタックサイズをヒープサイズによってのみ制限することは可能であると思われます。

Java仮想マシンスタックは、プッシュフレームとポップフレームを除いて直接操作されることはないため、フレームがヒープに割り当てられる場合があります。

そして

この仕様では、Java仮想マシンスタックを固定サイズにするか、計算の必要に応じて動的に拡張および縮小することができます。Java仮想マシンスタックの場合は固定サイズであり、各Java仮想マシンスタックのサイズは、そのスタックの作成時に個別に選択できます。

Java仮想マシンの実装により、プログラマーまたはユーザーは、動的な場合と同様に、Java仮想マシンスタックの初期サイズを制御できます。拡張または縮小Java仮想マシンスタック、最大サイズと最小サイズを制御します。

したがって、JVMの実装次第です。

10
Ryan Stewart

NPEへの追加の回答:

最大スタック深度はflexibleのようです。次のテストプログラムは、大幅に異なる数値を出力します。

public class StackDepthTest {
    static int i = 0;

    public static void main(String[] args) throws Throwable {
        for(int i=0; i<10; ++i){
            testInstance();
        }
    }

    public static void testInstance() {
        StackDepthTest sdt = new StackDepthTest();
        try {
            i=0;
            sdt.instanceCall();
        } catch(StackOverflowError e){}
        System.out.println(i);
    }

    public void instanceCall(){
        ++i;
        instanceCall();
    }
}

出力は次のとおりです。

10825
10825
59538
59538
59538
59538
59538
59538
59538
59538

このJREのデフォルトを使用しました:

Java version "1.7.0_09"
OpenJDK Runtime Environment (IcedTea7 2.3.3) (7u9-2.3.3-0ubuntu1~12.04.1)
OpenJDK 64-Bit Server VM (build 23.2-b09, mixed mode)

したがって、結論は次のとおりです。膿が十分にある場合(つまり、2回以上)、2回目のチャンスがあります;-)

4
A.H.

答えは、それはすべて異なります。

まず、Javaスタックサイズを変更できます。

第二に、特定のメソッドのスタックフレームサイズは、さまざまな変数に基づいて変化する可能性があります。詳細については、 コールスタック-ウィキペディア のコールスタックフレームサイズセクションを参照してください。

3
Justin Niessner

経験的な道を歩み、コードと-Xss設定を試してみることができます。詳細については、こちらを参照してください: JVMオプション-Xss-正確には何をしますか?

2
David Soroko

システムアーキテクチャ(32ビットまたは64ビットアドレス)、ローカル変数の量、およびメソッドのパラメータによって異なります。 tail-recursive オーバーヘッドがない場合、コンパイラーがループに最適化する場合。

ほら、簡単な答えはありません。

2
MrSmith42