Ruby执行尾部调用优化吗?

时间:2009-05-05 12:03:13

标签: ruby functional-programming tail-recursion

功能语言导致使用递归来解决许多问题,因此其中许多都执行尾调用优化(TCO)。 TCO导致从另一个函数调用函数(或者本身,在这种情况下,这个特性也称为Tail Recursion Elimination,它是TCO的一个子集),作为该函数的最后一步,不需要新的堆栈帧,这减少了开销和内存使用。

Ruby显然已经从函数式语言(lambdas,map等等函数)中“借用”了许多概念,这让我很好奇:Ruby是否执行尾调优化?

5 个答案:

答案 0 :(得分:123)

不,Ruby不执行TCO。但是,它也不会执行TCO。

Ruby语言规范没有提及TCO的任何内容。它并没有说你必须这样做,但它也没有说你不能这样做。你不能依赖就可以了。

这与Scheme不同,其中语言规范要求 所有实现必须执行TCO。但它也与Python不同,Guido van Rossum在多次(几天前的最后一次)非常清楚Python实现不应该执行TCO。

Yukihiro Matsumoto对TCO表示同情,他只是不想强制所有实现来支持它。不幸的是,这意味着您不能依赖TCO,或者如果您这样做,您的代码将不再可移植到其他Ruby实现。

因此,一些Ruby实现执行TCO,但大多数不执行。例如,YARV支持TCO,尽管(目前)你必须在源代码中明确取消注释一行并重新编译VM,以激活TCO - 在未来版本中,默认情况下,它将在实现证明之后启用稳定。 Parrot虚拟机本身支持TCO,因此Cardinal也可以非常轻松地支持它。 CLR对TCO有一些支持,这意味着IronRuby和Ruby.NET可能会这样做。 Rubinius也可能会这样做。

但JRuby和XRuby不支持TCO,他们可能不会,除非JVM本身获得对TCO的支持。问题在于:如果您希望快速实现,并与Java快速无缝集成,那么您应该与Java堆栈兼容并尽可能使用JVM的堆栈。您可以使用trampolines或显式的continuation-passing方式轻松实现TCO,但是您不再使用JVM堆栈,这意味着每次要调用Java或从Java调用Ruby时,都必须执行某种类型的转换,这很慢。因此,XRuby和JRuby选择了速度和Java集成而不是TCO和延续(基本上存在同样的问题)。

这适用于所有希望与某些本地不支持TCO的主机平台紧密集成的Ruby实现。例如,我猜MacRuby会遇到同样的问题。

答案 1 :(得分: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。它还解释了随之而来的一些问题:Tail call optimization: enable by default?.

链接的简短摘录:

  

通常,尾递归优化包括另一种优化技术 - “调用”到“跳转”转换。在我看来,   因为识别,很难应用这种优化   在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

答案 2 :(得分:12)

它可以但不能保证:

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

答案 3 :(得分:4)

在编译之前,还可以通过在vm_opts.h中调整几个变量来编译TCO: 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

答案 4 :(得分:2)

这建立在Jörg和Ernest的答案之上。 基本上它取决于实施。

我无法得到欧内斯特对MRI工作的答案,但这是可行的。 我发现this example适用于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