コンパイラーは、再帰ロジックを同等の非再帰ロジックに変換できますか?


15

私はF#を学んでおり、C#をプログラミングしているときの考え方に影響を与え始めています。そのために、結果が読みやすくなると感じたときに再帰を使用していましたが、スタックオーバーフローに巻き込まれることを想像できません。

これは、コンパイラーが再帰関数を同等の非再帰形式に自動的に変換できるかどうかを尋ねることになりますか?


末尾呼び出しの最適化は、基本的な例が、それだけで、あなたが持っている場合は動作するかどうかは良いですreturn recursecall(args);再帰のために、より複雑なものは、明示的なスタックを作成し、それを下巻きすることにより可能であるが、私は、彼らはないだろう
ラチェットフリーク

@ratchet freak:再帰は「スタックを使用している計算」を意味するものではありません。
ジョルジオ

1
@Giorgio私は知っていますが、スタックは再帰をループに変換する最も簡単な方法です
ラチェットフリーク

回答:


21

はい、一部の言語およびコンパイラは、再帰的ロジックを非再帰的ロジックに変換します。これは、末尾呼び出しの最適化として知られています-すべての再帰呼び出しが末尾呼び出しの最適化ではないことに注意してください。この状況では、コンパイラは次の形式の関数を認識します。

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:
.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

(新しいスタックフレームでサブルーチン呼び出しを行う)ではなく、segment .L4で見ることができます。jnecall

これはCで行われたことに注意してください。Javaでの末尾呼び出しの最適化は難しく、JVM実装に依存します。末尾再帰+ Javaおよび末尾再帰+最適化は、参照するのに適したタグセットです。あなたは他のJVM言語が最適化末尾再帰よりよい(必要とトライClojureの(することができます見つけることができRECUR末尾呼び出しの最適化への)、またはスカラ)。


1
これがOPが求めているものかどうかはわかりません。ランタイムが特定の方法でスタック領域を消費する、または消費しないからといって、関数が再帰的ではないという意味ではありません。

1
@MattFenwickどういう意味ですか?「これは、コンパイラーが再帰関数を同等の非再帰形式に自動的に変換できるかどうかを尋ねることにつながります」-答えは「特定の条件下ではyes」です。条件は実証されており、私が述べたテールコールの最適化を行う他の特定の一般的な言語にはいくつかの落とし穴があります。

9

慎重に踏んでください。

答えはイエスですが、常にではなく、すべてではありません。これはいくつかの異なる名前で行われる手法ですが、ここウィキペディアでかなり決定的な情報を見つけることができます。

私は「Tail Call Optimization」という名前を好みますが、他にも名前があり、一部の人々はこの用語を混同します。

とはいえ、実現すべき重要なことがいくつかあります。

  • テールコールを最適化するには、テールコールには、コールが行われた時点で既知のパラメーターが必要です。つまり、パラメーターの1つが関数自体の呼び出しである場合、ループに変換できません。これは、コンパイル時に展開できないループの任意のネストが必要になるためです。

  • C#は、末尾呼び出しを確実に最適化しませ。ILには、F#コンパイラが発行する命令がありますが、C#コンパイラは一貫性なく発行し、JITの状況に応じて、JITはまったく発行しない場合もあります。すべての兆候は、C#で最適化されているテールコールに依存するべきではないということです、そうすることでオーバーフローのリスクは重大で現実です


1
これがOPが求めているものだと確信していますか?ランタイムが特定の方法でスタックスペースを消費する、または消費しないという理由だけで、他の回答で投稿したように、関数が再帰的ではないという意味ではありません。

1
@MattFenwickは実際には素晴らしい点です、実際にはそれが依存します、テールコール命令を発行するF#コンパイラは再帰的なロジックを完全に維持していますしかし、他のコンパイラは文字通りループにコンパイルされる可能性があります。(技術的にJITがループまたは可能性もループレスファッションにコンパイルされるループは、全アップフロントある場合)
ジミー・ホッファ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.