Tail Call优化是否能解释这种性能差异

时间:2014-04-18 21:55:00

标签: ruby recursion jruby tail-recursion

我看到了三种不同的方式来编写Fibonacci函数的递归形式:数学内联,数学内联结果缓存和一个使用尾递归。我理解使用memoization在缓存答案后将O(N)算法转换为O(1)。但我无法理解尾调用优化如何帮助这么多。我的印象是它可能会阻止一些副本或类似的东西。它几乎和O(1)一样快。 Ruby做的是什么让这么快?

这是数学内联的慢速天真实现。它显然是运行时间最慢的O(N)时间,然后在O(N ^ 2)时间内显示为循环。

puts Benchmark.measure {
  # Calculate the nth Fibonacci number, f(n).
  def fibo (n)
    if n <= 1
      return n
    else
      value = fibo(n-1) + fibo(n-2)
      return value
    end
  end

  # Display the Fibonacci sequence.
  (1..40).each do |number|
    puts "fibo(#{number}) = #{fibo(number)}"
  end
}

Times Ruby 1.9.3:55.989000 0.000000 55.989000(55.990000)
Times JRuby 1.7.9:51.629000 0.000000 51.629000(51.629000)
来源(http://rayhightower.com/blog/2014/04/12/recursion-and-memoization/?utm_source=rubyweekly

这是备忘录的版本,很清楚为什么这对我来说很快。完成数学后,任何后续请求都会在O(1)时间内运行,因此当它包含在循环中时,在最坏的情况下它仍会在O(N)时间内运行:

puts Benchmark.measure {
  # Fibonacci numbers WITH memoization.

  # Initialize the memoization array.
  @scratchpad = []
  @max_fibo_size = 50
  (1..@max_fibo_size).each do |i|
    @scratchpad[i] = :notcalculated
  end

  # Calculate the nth Fibonacci number, f(n).
  def fibo (n)
    if n > @max_fibo_size
      return "n must be #{@max_fibo_size} or less."
    elsif n <= 1
      return n
    elsif @scratchpad[n] != :notcalculated
      return @scratchpad[n]
    else
      @scratchpad[n] = fibo(n-1) + fibo(n-2)
      return @scratchpad[n]
    end
  end

  # Display the Fibonacci sequence.
  (1..40).each { |number|
    puts "fibo(#{number}) = #{fibo(number)}"
  }
}

Times Ruby 1.9.3:0.000000 0.000000 0.000000(0.025000)
次JRuby 1.7.9:0.027000 0.000000 0.027000(0.028000)
来源(http://rayhightower.com/blog/2014/04/12/recursion-and-memoization/?utm_source=rubyweekly

版本尾调用递归版本可以立即运行:

puts Benchmark.measure {
  # Calculate the nth Fibonacci number, f(n). Using invariants
  def fibo_tr(n, acc1, acc2)
    if n == 0
      0
    elsif n < 2
      acc2
    else
      return fibo_tr(n - 1, acc2, acc2 + acc1)
    end
  end

  def fibo (n)
    fibo_tr(n, 0, 1)
  end 

  # Display the Fibonacci sequence.
  (1..50).each do |number|
    puts "fibo(#{number}) = #{fibo(number)}"
  end
}

Times Ruby 1.9.3:0.000000 0.000000 0.000000(0.021000)
次JRuby 1.7.9:0.041000 0.000000 0.041000(0.041000)
来源(https://gist.github.com/mvidaurre/11006570

2 个答案:

答案 0 :(得分:4)

尾递归不是这里的区别。事实上,Ruby没有做任何事情来优化尾调用。

不同之处在于,naive算法在每次调用时递归调用自身两次,得到O(2 n )性能,这意味着运行时随着N的增加呈指数上升。尾部调用版本以线性时间运行。

答案 1 :(得分:3)

<强> TL; DR: 正如Chuck已经提到的,Ruby没有TCO。但是,执行一次递归而不是两次递归会对您使用的堆栈数量以及完成的迭代次数产生很大影响。有了这个答案,我只想指出,在某些时候,memoization版本比迭代版本更好。注意:我不是一个红宝石程序员。它可能不是惯用代码。

测试显示迭代方法非常快,它可以从头开始生成1..50的fib,就像你的memoization版本在3以上的每个方法调用中重用计算一样快。

我认为1..50的速度非常快,以至于看迭代实际上是否更快是不可靠的。我将memopization版本更改为:

# Initialize the memoization array.
@scratchpad = []

# Calculate the nth Fibonacci number, f(n).
def fibo (n)
  if n <= 1 
    return n
  end
  if @scratchpad[n].nil?
    @scratchpad[n] = fibo(n-1) + fibo(n-2)
  end
  return @scratchpad[n]
end

然后我将循环更改为:

(1..5000).each { |number|
  fibo(number) # no need to time character output
}

以下是我计算机上的结果:

Iteration:   6.260000   0.010000   6.270000 (  6.273362)
Memoization: 0.000000   0.000000   0.000000 (  0.006943)

我用过:

ruby -v
ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-linux]

将memoization版本增加到1..50000仍然比迭代版本快得多。原因是每次迭代都是从头开始,而memoization版本的算法更无效,但是memoization使得它只为每个数字递归两次,因为我们有fib(n-1)fib(n-2) in the array when calculating fib(n )`。

最慢的当然是O(fib(n))。迭代有O(n)。通过记忆,fib(n-2)在计算fib(n-1)时是免费的,所以我们回到O(n)但是在你的测试中你计算了下一个之前的斐波纳契数,所以在实践中每个迭代都来自{{1}是1..x。如果您从最高编号开始,则第一次迭代将为O(1),而下一次迭代将为O(n)