Выполняет ли Ruby Tail Call Optimization?

Функциональные языки приводят к использованию рекурсии для решения множества проблем, поэтому многие из них выполняют оптимизацию Tail Call Optimization (TCO). TCO вызывает вызовы функции из другой функции (или самой себя, и в этом случае эта функция также известна как Tail Recursion Elimination, которая является подмножеством TCO), в качестве последнего шага этой функции, чтобы не требовать новый стек стека, что уменьшает накладные расходы и использование памяти.

Ruby, очевидно, "заимствовал" ряд понятий из функциональных языков (lambdas, функции, такие как карта и т.д. и т.д.), что заставляет меня любопытно: оптимизирует ли Ruby оптимизацию вызовов?

Ответ 1

Нет, Ruby не выполняет TCO. Однако он также не выполняет TCO.

Спецификация языка Ruby ничего не говорит о TCO. Он не говорит, что вам нужно это делать, но он также не говорит, что вы не можете этого сделать. Вы просто не можете полагаться на это.

Это не похоже на Схему, где спецификация языка требует, чтобы все Реализации выполняли TCO. Но он также не похож на Python, где Guido van Rossum очень много раз говорил (в последний раз всего пару дней назад), что реализации Python не должны выполнять TCO.

Юкихиро Мацумото сочувствует ТШО, он просто не хочет, чтобы все Реализации поддерживали его. К сожалению, это означает, что вы не можете полагаться на TCO, или если вы это сделаете, ваш код больше не будет переноситься на другие Ruby-реализации.

Итак, некоторые Ruby-реализации выполняют TCO, но большинство из них этого не делают. Например, YARV поддерживает TCO, хотя (на данный момент) вы должны явно раскомментировать строку в исходном коде и перекомпилировать виртуальную машину, чтобы активировать TCO - в будущих версиях она будет включена по умолчанию, после того, как реализация докажет стабильный. Виртуальная машина Parrot поддерживает TCO изначально, поэтому кардинал тоже вполне мог ее поддержать. CLR поддерживает некоторую поддержку TCO, что означает, что IronRuby и Ruby.NET могли бы это сделать. Возможно, Рубиний тоже мог бы это сделать.

Но JRuby и XRuby не поддерживают TCO, и они, вероятно, не будут, если только JVM не получит поддержку TCO. Проблема в том, что: если вы хотите иметь быструю реализацию и быструю и плавную интеграцию с Java, тогда вы должны быть совместимы с Java и использовать стек JVM как можно больше. Вы можете с легкостью реализовать TCO с батутами или явным стилем продолжения прохождения, но тогда вы больше не используете стек JVM, а это означает, что каждый раз, когда вы хотите позвонить в Java или вызвать Java в Ruby, вам нужно выполнить какой-то конверсия, которая медленная. Итак, XRuby и JRuby решили перейти со скоростью и интеграцией Java по TCO и продолжениям (которые в основном имеют одинаковую проблему).

Это относится ко всем реализациям Ruby, которые хотят тесно интегрироваться с некоторой платформой хоста, которая не поддерживает TCO изначально. Например, я предполагаю, что у MacRuby будет такая же проблема.

Ответ 2

Обновление: Здесь хорошее объяснение TCO в Ruby: http://nithinbekal.com/posts/ruby-tco/

Обновление: Возможно, вы также захотите проверить драгоценный камень tco_method: 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
}

Было предложено включить TCO по умолчанию в Ruby 2.0. Это также объясняет некоторые проблемы, которые приходят с этим: Оптимизация звонков: включить по умолчанию?.

Короткая выдержка из ссылки:

Как правило, оптимизация хвостовой рекурсии включает в себя еще один метод оптимизации - "вызов" для перехода "прыгать". По моему мнению, трудно применить эту оптимизацию, поскольку распознавание "Рекурсия" сложна в мире Ruby.

Следующий пример. вызов метода fact() в предложении "else" не является "хвостом" вызов".

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

Ответ 4

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

Ответ 5

Это основывается на ответах Йорга и Эрнеста. В основном это зависит от реализации.

Я не мог заставить Эрнеста отвечать на МРТ, но это выполнимо. Я нашел этот пример, который работает для 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