[英]Why is tail recursion optimization faster than normal recursion in Python?
雖然我知道尾部遞歸優化是非Pythonic的,但我想到了一個快速入侵這里的問題,一旦我准備發布就刪除了。
由於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%的代價是更大堆棧的開銷。
我在每次只運行一次時看到執行時間如此巨大差異的原因是分配堆棧內存和結構的代價。 一旦分配,使用它們的成本就會大大降低。
因為我的算法很簡單,所以內存結構分配是執行時間的很大一部分。
當我將我的堆棧啟動調用切換到find_zero(998)
unrelated_recursion(499)
,我在find_zero(998)
執行時間中完全啟動和未啟動堆棧之間的大約一半。 這與理論有關。
作為一個評論,希望能引起我的注意,我並沒有真正回答這個問題,所以這是我的觀點:
在優化中,你正在分配,解包和解除分配元組,所以我嘗試了沒有它們:
# Function that returns tuple of [method], [call value]
def find_zero_tail(num):
if num == 0:
return None
return num - 1
# Iterative recurser
def tail_optimize(method, val):
while val:
val = method(val)
return val
1000次嘗試,每次嘗試以值= 998開頭:
(請注意,對我來說,您的優化版本比未經優化的版本更快......但我們不會進行完全相同的測試。)
但是我不認為這對於獲取這些統計數據是有用的:成本更多的是在Python(方法調用,元組分配,...)方面,你的代碼在做真實的事情。 在實際應用程序中,您最終不會測量1000個元組的成本,而是實際實現的成本。
但是根本就不要這樣做:這幾乎沒有什么難以閱讀,你是為讀者而不是為機器而寫的:
# 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
我不會嘗試實現更可讀的版本,因為我可能會最終得到:
def find_zero(val):
return 0
但我認為在實際情況下有一些很好的方法來處理遞歸限制(內存大小或深度方面):
為了幫助記憶 (不是深度),來自functools的lru_cache通常可以提供很多幫助:
>>> from functools import lru_cache
>>> @lru_cache()
... def fib(x):
... return fib(x - 1) + fib(x - 2) if x > 2 else 1
...
>>> fib(100)
354224848179261915075
對於堆棧大小,您可以使用list
或deque
,具體取決於您的上下文和用法,而不是使用語言堆棧。 根據確切的實現(當你實際上在堆棧中存儲簡單的子計算以重用它們時),它被稱為動態編程 :
>>> def fib(x):
... stack = [1, 1]
... while len(stack) < x:
... stack.append(stack[-1] + stack[-2])
... return stack[-1]
...
>>> fib(100)
354224848179261915075
但是,這里有使用您自己的結構而不是調用堆棧的好處,您並不總是需要保持整個堆棧繼續計算:
>>> def fib(x):
... stack = (1, 1)
... for _ in range(x - 2):
... stack = stack[1], stack[0] + stack[1]
... return stack[1]
...
>>> fib(100)
354224848179261915075
但總結一下,“在嘗試實現它之前先了解問題”(不可讀,難以調試,難以直觀地證明,這是糟糕的代碼,但它很有趣):
>>> def fib(n):
... return (4 << n*(3+n)) // ((4 << 2*n) - (2 << n) - 1) & ((2 << n) - 1)
...
>>>
>>> fib(99)
354224848179261915075
如果你問我,最好的實現是更可讀的(對於Fibonacci示例,可能是具有LRU緩存的那個,但是通過更改... if ... else ...
具有更易讀的if語句,用於另一個例如, deque
可能更具可讀性,而對於其他示例,動態編程可能更好......
“你正在為人類閱讀你的代碼而不是為機器而寫作”。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.