web-dev-qa-db-ja.com

Rubyは末尾呼び出しの最適化を実行しますか?

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

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

90
Charlie Flowers

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

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

これは、言語仕様必須すべて実装必須 TCOを実行するSchemeとは異なります。しかし、Guido van Rossumが複数回(前回は数日前)にPython実装すべきではない TCOを実行することを非常に明確にしたPythonとは異なります。

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

したがって、一部のRuby実装はTCOを実行しますが、ほとんどは実行しません。たとえば、YARVはTCOをサポートしますが、(現時点では)ソースコードの行のコメントを明示的に解除してVMを再コンパイルし、TCOをアクティブにする必要があります。将来のバージョンでは、実装が証明された後、デフォルトでオンになります。安定した。 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でも同じ問題が発生すると思います。

125
Jörg W Mittag

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

Update:また、tco_methodgemも確認してください:- 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をオンにする提案がありました。また、それに伴ういくつかの問題についても説明しています: Tailコールの最適化:デフォルトで有効にしますか?

リンクからの短い抜粋:

一般に、末尾再帰の最適化には別の最適化手法が含まれています。「呼び出し」から「ジャンプ」への変換です。私の意見では、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
42
Ernest

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

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

12
Steve Jessop

TCOは、コンパイルする前にvm_opts.hのいくつかの変数を微調整することでコンパイルすることもできます: https://github.com/Ruby/ruby/blob/trunk/vm_opts.h#L21

// vm_opts.h
#define OPT_TRACE_INSTRUCTION        0    // default 1
#define OPT_TAILCALL_OPTIMIZATION    1    // default 0

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

アーネストの答えを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
2
Kelvin