「JVMはテールコールの最適化をサポートしていないため、大量のスタックの爆発を予測しています」
これを言う人は誰でも、(1)末尾呼び出しの最適化を理解していない、(2)JVMを理解していない、または(3)両方です。
まず、Wikipediaからのテールコールの定義から始めます(Wikipediaが気に入らない場合は、代替手段があります)。
コンピューターサイエンスでは、末尾呼び出しは、最終的なアクションとして別のプロシージャ内で発生するサブルーチン呼び出しです。戻り値を生成し、呼び出し元のプロシージャによって直ちに返される場合があります
以下のコードでは、への呼び出しbar()
はの末尾呼び出しですfoo()
:
private void foo() {
// do something
bar()
}
テールコールの最適化は、テールコールを確認する言語実装が通常のメソッド呼び出し(スタックフレームを作成する)を使用せず、代わりにブランチを作成するときに発生します。スタックフレームにはメモリが必要であり、フレームに情報(リターンアドレスなど)をプッシュするにはCPUサイクルが必要であり、呼び出し/リターンペアは無条件ジャンプよりも多くのCPUサイクルを必要とするため、これは最適化です。
TCOは再帰によく適用されますが、それだけが使用されるわけではありません。また、すべての再帰に適用されるわけでもありません。たとえば、階乗を計算する単純な再帰的コードは、関数で最後に発生するのが乗算演算であるため、末尾呼び出しを最適化できません。
public static int fact(int n) {
if (n <= 1) return 1;
else return n * fact(n - 1);
}
テールコールの最適化を実装するには、次の2つが必要です。
- サブルーチン呼び出しに加えて分岐をサポートするプラットフォーム。
- テールコールの最適化が可能かどうかを判断できる静的アナライザー。
それでおしまい。別の場所で述べたように、JVM(他のチューリング完全なアーキテクチャと同様)には後藤があります。たまたま無条件のgotoがありますが、機能は条件分岐を使用して簡単に実装できます。
静的分析のピースは、トリッキーなものです。1つの関数内で問題ありません。たとえば、aの値を合計する末尾再帰Scala関数はList
次のとおりです。
def sum(acc:Int, list:List[Int]) : Int = {
if (list.isEmpty) acc
else sum(acc + list.head, list.tail)
}
この関数は次のバイトコードに変わります:
public int sum(int, scala.collection.immutable.List);
Code:
0: aload_2
1: invokevirtual #63; //Method scala/collection/immutable/List.isEmpty:()Z
4: ifeq 9
7: iload_1
8: ireturn
9: iload_1
10: aload_2
11: invokevirtual #67; //Method scala/collection/immutable/List.head:()Ljava/lang/Object;
14: invokestatic #73; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
17: iadd
18: aload_2
19: invokevirtual #76; //Method scala/collection/immutable/List.tail:()Ljava/lang/Object;
22: checkcast #59; //class scala/collection/immutable/List
25: astore_2
26: istore_1
27: goto 0
なお、goto 0
最後に。比較すると、同等のJava関数(Iterator
Scalaリストを先頭と末尾に分割する動作を模倣するために使用する必要がある)は、次のバイトコードに変わります。最後の2つの操作はinvokeであり、その再帰呼び出しによって生成された値が明示的に返されることに注意してください。
public static int sum(int, java.util.Iterator);
Code:
0: aload_1
1: invokeinterface #64, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z
6: ifne 11
9: iload_0
10: ireturn
11: iload_0
12: aload_1
13: invokeinterface #70, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
18: checkcast #25; //class java/lang/Integer
21: invokevirtual #74; //Method java/lang/Integer.intValue:()I
24: iadd
25: aload_1
26: invokestatic #43; //Method sum:(ILjava/util/Iterator;)I
29: ireturn
単一の関数の末尾呼び出しの最適化は簡単です。コンパイラーは呼び出しの結果を使用するコードがないことを確認できるため、呼び出しをに置き換えることができgoto
ます。
人生が難しいのは、複数の方法がある場合です。JVMの分岐命令は、80x86などの汎用プロセッサーの命令とは異なり、単一のメソッドに限定されています。プライベートメソッドがある場合でも、比較的簡単です:コンパイラーは、必要に応じてそれらのメソッドを自由にインライン化できるため、テールコールを最適化できます(これがどのように機能するのか疑問に思う場合は、a switch
を使用して動作を制御する一般的なメソッドを検討してください)。この手法を同じクラスの複数のパブリックメソッドに拡張することもできます。コンパイラはメソッド本体をインライン化し、パブリックブリッジメソッドを提供し、内部呼び出しはジャンプに変わります。
ただし、特にインターフェイスとクラスローダーを考慮して、異なるクラスのパブリックメソッドを検討すると、このモデルは機能しなくなります。ソースレベルのコンパイラには、テールコール最適化を実装するための十分な知識がありません。ただし、「ベアメタル」実装とは異なり、* JVM(は、これを行うための情報をHotspotコンパイラの形式で持っています(少なくとも、元Sunコンパイラが持っています)。実際に実行されるかどうかはわかりません。テールコールの最適化、および疑わないが、可能性があります。
これはあなたの質問の第2部に私を連れて行きます、私は「気にすべきか」と言い換えます。
明らかに、言語が反復の唯一のプリミティブとして再帰を使用している場合は、気になります。ただし、この機能を必要とする言語では実装できます。唯一の問題は、その言語のコンパイラが、任意のJavaクラスを呼び出して呼び出すことができるクラスを生成できるかどうかです。
そのケースの外で、私はそれが無関係であると言うことによってdownvotesを招待するつもりです。私が見た(そして多くのグラフプロジェクトで作業した)再帰コードのほとんどは、末尾呼び出しで最適化できません。単純な階乗と同様に、再帰を使用して状態を構築し、テール操作は組み合わせです。
末尾呼び出しで最適化可能なコードの場合、多くの場合、そのコードを反復可能な形式に変換するのは簡単です。例えば、そのsum()
先ほど示したこと関数は次のように一般化することができますfoldLeft()
。sourceを見ると、実際に反復操作として実装されていることがわかります。JörgW Mittagには、関数呼び出しを介して実装されたステートマシンの例がありました。ジャンプに変換される関数呼び出しに依存しない、効率的な(および保守可能な)ステートマシンの実装が多数あります。
完全に異なるもので終わります。SICPの脚注からグーグルで検索する場合、ここで終わるかもしれません。私は個人的に私のコンパイラは置き換えることよりもはるかに興味深い場所を見つけることJSR
によってJUMP
。