雖然我明白尾遞歸優化是非Pythonic,但我想出了一個快速入侵的問題,在我準備發佈後立即刪除。爲什麼尾遞歸優化比Python中的正常遞歸更快?
在1000堆棧限制下,深度遞歸算法在Python中不可用。但有時候,通過解決方案的初步想法是非常好的。由於函數是Python中的第一類,所以我使用返回一個有效的函數和下一個值來玩。然後在循環中調用該過程,直到完成單個調用。我相信這不是新的。
我發現有趣的是,我期望來回傳遞函數的額外開銷,使其比正常遞歸慢。在我的粗略測試中,我發現它需要30-50%的正常遞歸時間。 (隨着允許LONG遞歸額外的獎勵)
這裏是代碼我運行:
from contextlib import contextmanager
import time
# Timing code from StackOverflow most likely.
@contextmanager
def time_block(label):
start = time.clock()
try:
yield
finally:
end = time.clock()
print ('{} : {}'.format(label, end - start))
# Purely Recursive Function
def find_zero(num):
if num == 0:
return num
return find_zero(num - 1)
# Function that returns tuple of [method], [call value]
def find_zero_tail(num):
if num == 0:
return None, num
return find_zero_tail, num - 1
# Iterative recurser
def tail_optimize(method, val):
while method:
method, val = method(val)
return val
with time_block('Pure recursion: 998'):
find_zero(998)
with time_block('Tail Optimize Hack: 998'):
tail_optimize(find_zero_tail, 998)
with time_block('Tail Optimize Hack: 1000000'):
tail_optimize(find_zero_tail, 10000000)
# One Run Result:
# Pure recursion: 998 : 0.000372791020758
# Tail Optimize Hack: 998 : 0.000163852100569
# Tail Optimize Hack: 1000000 : 1.51006975627
爲什麼是第二快的風格?
我的猜測是在棧上創建條目的開銷,但我不知道如何找出答案。
編輯:
與調用次數玩,我做了一個循環,在不同的NUM值都試一下。當我多次循環和調用時,遞歸更接近平價。
所以,我的時機,這是find_zero下一個新的名字前面加上這樣的:
def unrelated_recursion(num):
if num == 0:
return num
return unrelated_recursion(num - 1)
unrelated_recursion(998)
現在的尾部優化通話的完整遞歸的85%的時間。
所以我的理論是,15%的懲罰是大堆棧的開銷,而不是單堆棧。
之所以我看到如此巨大的執行時間差異,只運行一次就是堆棧內存和結構分配的代價。一旦分配,使用它們的成本就大大降低了。
因爲我的算法很簡單,內存結構分配是執行時間的很大一部分。
當我將堆棧啓動調用切換到unrelated_recursion(499)
時,我在find_zero(998)
執行時間之間獲得了完全啓動和未啓動的堆棧之間的大約一半。這與理論有關。
可能歸結爲它只需分配一個堆棧幀而不是多個堆棧幀。它甚至可以從這樣一個事實中受益:分配器可能在每次迭代時爲新的幀對象返回相同的塊,因此它具有更好的緩存局部性。 –
切換呼叫訂單會稍微影響它。正常遞歸似乎在n <20時獲勝。在20到40之間,取決於呼叫順序,它們是平等的。尾遞歸在n> 40的情況下獲勝。所以它似乎與堆棧開銷有關。 – Joe
如果我用'timeit'模塊調用這些調用,tail調用優化版本贏得低重複計數,但另一個版本贏得高重複計數。 – user2357112