私はF#を学習してきましたが、C#をプログラミングするときの考え方に影響を与え始めています。そのため、私は結果が読みやすさを向上させ、スタックオーバーフローに巻き込まれるとは思えないときに再帰を使用していました。
これにより、コンパイラが再帰関数を同等の非再帰形式に自動的に変換できるかどうかを尋ねるようになりますか?
はい、一部の言語とコンパイラは、再帰ロジックを非再帰ロジックに変換します。これは 末尾呼び出しの最適化として知られています -すべての再帰呼び出しが末尾呼び出しを最適化できるわけではないことに注意してください。この状況では、コンパイラーは次の形式の関数を認識します。
int foo(n) {
...
return bar(n);
}
ここで、言語は、返されている結果が別の関数からの結果であることを認識し、新しいスタックフレームでの関数呼び出しをジャンプに変更できます。
古典的な階乗法を理解してください:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
返品には検査が必要なため、notテールコールは最適化可能です。
この末尾呼び出しを最適化するには、
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:
.LFB0:
.cfi_startproc
cmpl $1, %edi
movl %esi, %eax
je .L2
.p2align 4,,10
.p2align 3
.L4:
imull %edi, %eax
subl $1, %edi
cmpl $1, %edi
jne .L4
.L2:
rep
ret
.cfi_endproc
セグメント.L4
では、jne
ではなくcall
(新しいスタックフレームでサブルーチン呼び出しを行う)を確認できます。
これはCで行われたことに注意してください。Javaでの末尾呼び出しの最適化は困難であり、JVM実装に依存します- tail-recursion + Java および tail-recursion + optimization は、参照するのに適したタグセットです。他のJVM言語は、末尾再帰をより適切に最適化できる場合があります(clojureを試してください( recur (末尾呼び出し最適化)、またはscala)。
注意深く踏みます。
答えは「はい」ですが、常にではなく、すべてではありません。これはいくつかの異なる名前で呼ばれるテクニックですが、かなり明確な情報 here と wikipedia で見つけることができます。
私は「Tail Call Optimization」という名前を好んでいますが、他にもあり、一部の人々はこの用語を混乱させます。
とはいえ、実現すべき重要なことがいくつかあります。
末尾呼び出しを最適化するために、末尾呼び出しには、呼び出しが行われたときにわかっているパラメーターが必要です。つまり、パラメータの1つが関数自体への呼び出しである場合、それはループに変換できません。コンパイル時間。
C#はしないテールコールを確実に最適化します。 ILには、F#コンパイラーが発行する指示がありますが、C#コンパイラーはそれを一貫して発行せず、JITの状況によっては、JITがそれを実行する場合としない場合があります。 すべての兆候は、C#で最適化されているテールコールに依存するべきではないことを示しています。そうすることでのオーバーフローのリスクは重大かつ現実的です