テールコールの最適化は、多くの言語とコンパイラにあります。この状況では、コンパイラは次の形式の関数を認識します。
int foo(n) {
...
return bar(n);
}
ここで、言語は返される結果が別の関数からの結果であることを認識し、新しいスタックフレームを使用して関数呼び出しをジャンプに変更できます。
古典的な階乗法を実現する:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
ではないので、復帰に必要な検査のテールコールoptimizatableは。(ソースコードとコンパイル済み出力の例)
このテールコールを最適化するには、
int _fact(int n, int acc) {
if(n == 1) return acc;
return _fact(n - 1, acc * n);
}
int factorial(int n) {
if(n == 0) return 1;
return _fact(n, 1);
}
このコードをコンパイルしますgcc -O2 -S fact.c
(コンパイラで最適化を有効にするには-O2が必要ですが、-O3をさらに最適化すると、人間が読むのが難しくなります...)
_fact(int, int):
cmpl $1, %edi
movl %esi, %eax
je .L2
.L3:
imull %edi, %eax
subl $1, %edi
cmpl $1, %edi
jne .L3
.L2:
rep ret
(ソースコードとコンパイル済み出力の例)
(新しいスタックフレームでサブルーチン呼び出しを行う)ではなく、segment .L3
で見ることができます。jne
call
これはCで行われたことに注意してください。Javaでのテールコールの最適化は難しく、JVM実装に依存します(つまり、それを行うものは見たことがありません。 -これはTCOが回避するものです)- 末尾再帰+ javaおよび末尾再帰+最適化は、閲覧するのに適したタグセットです。あなたは他のJVM言語が最適化末尾再帰よりよい(必要とトライClojureの(することができます見つけることができRECUR末尾呼び出しの最適化への)、またはスカラ)。
とはいえ、
あなたが正しいことを書いたということを知ることには、それができる理想的な方法で、ある喜びがあります。
そして今、私はスコッチを手に入れ、ドイツのエレクトロニカを演奏します...
「再帰アルゴリズムのスタックオーバーフローを回避する方法」という一般的な質問に...
別のアプローチは、再帰カウンターを含めることです。これは、制御できない状況(およびコーディングの悪さ)によって引き起こされる無限ループを検出するためのものです。
再帰カウンターの形式は次のとおりです。
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
電話をかけるたびに、カウンターを増やします。カウンターが大きくなりすぎると、エラーが発生します(ここでは、-1が返されますが、他の言語では例外をスローすることもできます)。考えは、予想よりもはるかに深く、おそらく無限ループである再帰を実行するときに、最悪の事態(メモリ不足エラー)を防ぐことです。
理論的には、これは必要ありません。実際には、多数の小さなエラーと悪いコーディングプラクティス(別のスレッドが再帰呼び出しの無限ループに入るメソッドの外側で何かが変更されるマルチスレッド同時実行性の問題)が原因で、これにぶつかるコードの記述が不十分です。
適切なアルゴリズムを使用して、適切な問題を解決してください。具体的にコラッツの問題のために、表示され、あなたがそれを解決しようとしていることをXKCD方法:
あなたは数字から始めて、ツリーをたどっています。これにより、検索スペースが非常に大きくなります。正解の反復回数を計算するクイックランは、約500ステップになります。これは、小さなスタックフレームでの再帰では問題になりません。
再帰的な解決策を知ることは悪いことではありませんが、反復的な解決策の方が何倍も優れていることも認識しておく必要があります。再帰アルゴリズムから反復アルゴリズムへの変換にアプローチする多くの方法は、再帰から反復に進む方法のスタックオーバーフローで見ることができます。