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