Rubyはテールコールの最適化を実行しますか?


92

関数型言語は、多くの問題を解決するために再帰を使用することになるため、それらの多くはTail Call Optimization(TCO)を実行します。TCOは、その関数の最後のステップとして、新しいスタックフレームを必要としないように、別の関数(またはこの関数自体、この機能はTCOのサブセットであるテール再帰除去とも呼ばれます)から関数を呼び出します。オーバーヘッドとメモリ使用量が減少します。

Rubyは明らかに関数型言語から多くの概念(ラムダ、マップなどの関数など)を「借用」しているため、奇妙なことに、Rubyは末尾呼び出しの最適化を実行しますか?

回答:


127

いいえ、RubyはTCOを実行しません。ただし、TCO 実行されません。

Ruby言語仕様はTCOについて何も述べていません。それはあなたがそれをしなければならないというわけではありませんが、あなたがそれを行うことができないとも言いません。それに頼ることはできません。

これは、言語仕様は、スキーム、とは違っている必要があること、すべての実装はしなければならない TCOを行います。ただし、Guido van RossumがPython実装 TCOを実行すべきでないことを複数回(前回は数日前)非常に明確にしたPythonとは異なります。

松本幸宏はTCOに同情しています。彼はすべての実装にTCOのサポートを強制たくありません。残念ながら、これはTCOに依存できないことを意味します。そうしないと、コードは他のRuby実装に移植できなくなります。

そのため、一部のRuby実装はTCOを実行しますが、ほとんどは実行しません。たとえば、YARVはTCOをサポートしていますが、(現時点では)TCOをアクティブにするには、ソースコードの行のコメントを明示的に解除してVMを再コンパイルする必要があります。将来のバージョンでは、実装が証明された後、デフォルトでオンになります安定した。Parrot Virtual MachineはTCOをネイティブでサポートしているため、Cardinalも非常に簡単にサポートできます。CLRはTCOをある程度サポートしています。つまり、IronRubyとRuby.NETはおそらくそれを実行できます。ルビニウスもおそらくそうすることができたでしょう。

ただし、JRubyとXRubyはTCOをサポートしていません。JVM自体がTCOのサポートを得ない限り、おそらくそれらはサポートしません。問題はこれです:高速な実装とJavaとの高速でシームレスな統合が必要な場合は、Javaとスタック互換で、JVMのスタックをできるだけ使用する必要があります。トランポリンや明示的な継続渡しスタイルを使用してTCOを非常に簡単に実装できますが、JVMスタックを使用しなくなりました。つまり、Javaを呼び出したり、JavaからRubyを呼び出したりするたびに、何らかの種類の処理を実行する必要があります。変換は遅いです。そのため、XRubyとJRubyは、TCOと継続(基本的に同じ問題)を超える速度とJava統合を採用することにしました。

これは、TCOをネイティブにサポートしていないホストプラットフォームと緊密に統合したいRubyのすべての実装に適用されます。たとえば、MacRubyでも同じ問題が発生すると思います。


2
私は誤解しているかもしれませんが(そうだとしたら教えてください)、TCOは呼び出し元のスタックフレームを再利用できる必要があるため、真のOO言語ではTCOが意味をなさないと思います。遅延バインディングでは、メッセージの送信によってどのメソッドが呼び出されるかはコンパイル時に分からないため、タイプフィードバックJITを使用するか、メッセージのすべての実装者にスタックフレームの使用を強制することによって、それを確実にすることは難しいようです。同じサイズ、またはTCOを同じメッセージの自己送信に制限することにより…)。
Damien Pollet、

2
それは素晴らしい反応です。その情報は、Google経由で簡単に見つけることはできません。yarvがサポートしているのは興味深いことです。
チャーリーフラワー

15
ダミアン、実際のOO言語にはTCOが実際に必要であることがわかりました。projectfortress.sun.com/ Projects / Community / blog /…を参照してください。スタックフレームに関することをあまり気にしないでください。TCOとうまく機能するように、スタックフレームを慎重に設計することは完全に可能です。
Tony Garnock-Jones

2
tonygはGLSの参照された投稿を絶滅から保存し、ここにミラーリングしました:eighty-twenty.org/index.cgi/tech/oo-tail-calls-20111001.html
Frank Shearar

私は、任意の深さのネストされた配列のセットを分解する必要がある宿題をしています。それを行う明白な方法は再帰的であり、オンラインで(私が見つけることができる)同様のユースケースは再帰を使用します。私の特定の問題は、TCOがなくても爆発する可能性はほとんどありませんが、反復に切り替えずに完全に一般的なソリューションを記述できないという事実は、私を困らせます。
Isaac Rabinovitch 2013年

42

更新: RubyでのTCOの良い説明は次のとおりです:http : //nithinbekal.com/posts/ruby-tco/

更新:tco_method gem も確認してくださいhttp : //blog.tdg5.com/introducing-the-tco_method-gem/

Ruby MRI(1.9、2.0、および2.1)では、次のコマンドでTCOをオンにできます。

RubyVM::InstructionSequence.compile_option = {
  :tailcall_optimization => true,
  :trace_instruction => false
}

Ruby 2.0では、デフォルトでTCOをオンにするという提案がありました。また、それに伴ういくつかの問題についても説明します。末尾呼び出しの最適化:デフォルトで有効にしますか?。

リンクからの短い抜粋:

一般に、末尾再帰の最適化には別の最適化手法が含まれます-「呼び出し」から「ジャンプ」への変換。Rubyの世界では「再帰」の認識が難しいため、この最適化を適用するのは難しいと思います。

次の例。「else」句でのfact()メソッドの呼び出しは、「末尾呼び出し」ではありません。

def fact(n) 
  if n < 2
    1 
 else
   n * fact(n-1) 
 end 
end

fact()メソッドで末尾呼び出しの最適化を使用する場合は、fact()メソッドを次のように変更する必要があります(継続渡しスタイル)。

def fact(n, r) 
  if n < 2 
    r
  else
    fact(n-1, n*r)
  end
end

12

次のことが可能ですが、保証はされません。

https://bugs.ruby-lang.org/issues/1256


現在、リンクは機能していません。
空手犬2014

@karatedog:ありがとう、更新されました。正直なところ、この参照はおそらく古くなっていますが、バグは現在5年前のものであり、それ以来同じテーマで活動が行われています。
スティーブジェソップ2014

はい:-)トピックについて読んだところ、Ruby 2.0ではソースコードから有効にできることがわかりました(Cソースの変更や再コンパイルは不要です)。
空手犬2014


2

これは、ヨルクとアーネストの答えに基づいています。基本的には実装に依存します。

アーネストの答えをMRIで処理することはできませんでしたが、それは可能です。MRI 1.9〜2.1で機能するこの例を見つけました。これは非常に大きな数を出力するはずです。TCOオプションをtrueに設定しない場合、「スタックが深すぎる」​​エラーが発生するはずです。

source = <<-SOURCE
def fact n, acc = 1
  if n.zero?
    acc
  else
    fact n - 1, acc * n
  end
end

fact 10000
SOURCE

i_seq = RubyVM::InstructionSequence.new source, nil, nil, nil,
  tailcall_optimization: true, trace_instruction: false

#puts i_seq.disasm

begin
  value = i_seq.eval

  p value
rescue SystemStackError => e
  p e
end
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.