Ruby是否执行尾部调用优化?


92

函数式语言导致使用递归来解决许多问题,因此许多函数式语言执行尾部调用优化(TCO)。TCO导致从另一个函数(或其本身,在这种情况下也称为“尾递归消除”,它是TCO的子集)对该函数的调用,作为该函数的最后一步,不需要新的堆栈框架,这减少了开销和内存使用。

Ruby显然从功能语言(lambda,地图等功能)中“借用”了许多概念,这使我感到好奇:Ruby是否执行尾部调用优化?

Answers:


127

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

Ruby语言规范未提及TCO。它并没有说您必须这样做,但也没有说您不能这样做。您就是不能依靠它。

这与计划,其中语言规范要求的是所有的实现必须进行TCO。但是它也与Python不同,在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可以做到这一点。鲁比尼乌斯也可以做到。

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

这适用于所有想要与某些本身不支持TCO的主机平台紧密集成的Ruby实现。例如,我猜MacRuby将有同样的问题。


2
我可能会弄错(如果可以的话,请告诉我),但是我怀疑TCO在真正的OO语言中是否有意义,因为尾部调用必须能够重用调用方堆栈框架。由于使用后期绑定,因此在编译时尚不知道消息发送将调用哪种方法,因此似乎很难确保(也许使用类型反馈JIT或通过强制消息的所有实现者使用堆栈框架)大小相同,或将TCO限制为同一邮件的自发...)。
Damien Pollet

2
这是一个很好的回应。通过Google很难找到该信息。有趣的是yarv支持它。
查理·弗罗里斯2009年

15
Damien,事实证明,真正的OO语言实际上需要 TCO :请参阅projectfortress.sun.com/Projects/Community/blog/…。不必太担心堆栈框架的内容:明智地设计堆栈框架以使其与TCO完美配合是完全可能的。
Tony Garnock-Jones 2010年

2
tonyg将GLS的参考帖子从灭绝中保存下来,在此处进行了镜像:八十-twenty.org/index.cgi/tech/oo-tail-calls-20111001.html
Frank Shearar 2011年

我正在做一项作业,要求我拆解一组任意深度的嵌套数组。显而易见的方法是递归,类似的在线在线用例(我可以找到)使用递归。即使没有TCO,我的特殊问题也不太可能解决,但事实上,如果不切换到迭代就无法编写一个完全通用的解决方案。
艾萨克·拉比诺维奇

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

如果要对事实()方法使用尾部调用优化,则需要按以下方式更改事实()方法(连续传递样式)。

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

12

它可以具有但不能保证:

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


到目前为止,该链接已失效。
karatedog

@karatedog:谢谢,更新。虽然说实话,参考可能已经过时了,但由于该错误已经存在5年了,并且此后一直在进行同一主题的活动。
史蒂夫·杰索普

是的:-)我刚刚读到该主题,并且看到在Ruby 2.0中可以从源代码启用它(不再需要C源代码修改和重新编译)。
karatedog 2014年


2

这建立在约尔格和欧内斯特的答案上。基本上,它取决于实现。

我无法获得欧内斯特(Ernest)对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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.