[英]Is Tail Call optimization all that explains this performance difference
我看到了三種不同的方法來編寫斐波那契函數的遞歸形式:數學內聯,數學內聯結果緩存和一種使用尾遞歸。 我知道在緩存答案后,使用記憶將 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
}
時間 Ruby 1.9.3: 55.989000 0.000000 55.989000 ( 55.990000)
時代 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)}"
}
}
時間 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
}
時間 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 )
尾遞歸不是這里的區別。 事實上,Ruby 並沒有做任何優化尾調用的事情。
不同之處在於,朴素算法每次被調用時都會遞歸調用自身兩次,從而提供 O(2 n ) 性能,這意味着運行時間隨着 N 的增加呈指數增長。 尾調用版本以線性時間運行。
TL; DR:正如 Chuck 已經提到的,Ruby 沒有 TCO。 但是,執行一次遞歸而不是兩次遞歸對您使用的堆棧數量和完成的迭代次數有很大影響。 有了這個答案,我只想指出,有時記憶版本比迭代版本更好。 注意:我不是 ruby 程序員。 它可能不是慣用代碼。
測試表明迭代方法非常快,它可以從頭開始生成 1..50 的 fib,就像你的記憶版本在 3 以上的每個方法調用中重用計算一樣快。
我認為 1..50 完成得如此之快,以至於查看迭代實際上是否更快並不是很可靠。 我將備忘錄版本更改為:
# 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]
將記憶版本增加到 1..50000 仍然比迭代版本快很多。 原因是每次迭代都從頭開始,而記憶版本有一個更無效的算法,但是記憶使得每個數字最多只能遞歸兩次,因為我們有fib(n-1)
和fib(n-2) in the array when calculating
fib(n)` fib(n-2) in the array when calculating
。
最慢的當然是O(fib(n))
。 迭代具有O(n)
。 通過記憶, fib(n-2)
在計算fib(n-1)
時是免費的,所以我們回到O(n)
但在您的測試中,您在下一個之前計算前一個斐波那契數,因此在實踐中每個單獨的迭代從1..x
是O(1)
。 如果你從最大的數字開始,第一次迭代將是O(n)
,接下來的每一次都是O(1)
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.