Хотя я понимаю, что оптимизация хвостовой рекурсии не является 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
Почему второй стиль быстрее?
Мое предположение - накладные расходы на создание записей в стеке, но я не уверен, как это узнать.
Edit:
При игре с подсчетами звонков, я сделал цикл, чтобы попробовать оба с различными значениями 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)
времени выполнения. Это имеет смысл с теорией.