Прелюдия
У меня есть две реализации для конкретной проблемы, одна рекурсивная и одна итеративная, и я хочу знать, что заставляет итеративное решение быть на 30% медленнее, чем рекурсивное.
Учитывая рекурсивное решение, я пишу итерационное решение, делающее стек явным.
Ясно, что я просто имитирую то, что делает рекурсия, поэтому, конечно, движок Python лучше оптимизирован для обработки бухгалтерского учета. Но можно ли написать итерационный метод с аналогичной производительностью?
Мое тематическое исследование Проблема №14 в Project Euler.
Найдите самую длинную цепочку Collatz с начальным числом ниже миллиона.
Код
Вот рецессивное решение (кредит, связанный с веритами в нити проблемы плюс оптимизация от jJjjJ):
def solve_PE14_recursive(ub=10**6):
def collatz_r(n):
if not n in table:
if n % 2 == 0:
table[n] = collatz_r(n // 2) + 1
elif n % 4 == 3:
table[n] = collatz_r((3 * n + 1) // 2) + 2
else:
table[n] = collatz_r((3 * n + 1) // 4) + 3
return table[n]
table = {1: 1}
return max(xrange(ub // 2 + 1, ub, 2), key=collatz_r)
Здесь моя итеративная версия:
def solve_PE14_iterative(ub=10**6):
def collatz_i(n):
stack = []
while not n in table:
if n % 2 == 0:
x, y = n // 2, 1
elif n % 4 == 3:
x, y = (3 * n + 1) // 2, 2
else:
x, y = (3 * n + 1) // 4, 3
stack.append((n, y))
n = x
ysum = table[n]
for x, y in reversed(stack):
ysum += y
table[x] = ysum
return ysum
table = {1: 1}
return max(xrange(ub // 2 + 1, ub, 2), key=collatz_i)
И тайминги на моей машине (машина i7 с большим объемом памяти) с использованием IPython:
In [3]: %timeit solve_PE14_recursive()
1 loops, best of 3: 942 ms per loop
In [4]: %timeit solve_PE14_iterative()
1 loops, best of 3: 1.35 s per loop
Комментарии
Рекурсивное решение является удивительным:
- Оптимизирован, чтобы пропустить шаг или два в зависимости от двух младших значащих бит.
Мое первоначальное решение не пропустило никаких шагов Collatz и заняло ~ 1.86 с - Трудно попасть в предел рекурсии по умолчанию Python 1000.
collatz_r(
9780657630
)
возвращает 1133, но требует менее 1000 рекурсивных вызовов. - Воспоминание позволяет избежать повторной проверки.
-
collatz_r
длина, рассчитанная по запросуmax
Играя с ним, тайминги кажутся точными до +/- 5 мс.
Языки со статической типизацией, такие как C и Haskell, могут получить тайминги ниже 100 мс.
Я положил инициализацию memoization table
в методе по дизайну для этого вопроса, так что тайминги будут отражать "повторное обнаружение" значений таблиц при каждом вызове.
collatz_r(2**1002)
поднимает RuntimeError: maximum recursion depth exceeded
. collatz_i(2**1002)
счастливо возвращается с помощью 1003
.
Я знаком с генераторами, сопрограммами и декораторами.
Я использую Python 2.7. Я также рад использовать Numpy (1,8 на моей машине).
Что я ищу
- итерационное решение, которое закрывает разрыв производительности
- обсуждение того, как Python обрабатывает рекурсию
- более тонкие детали штрафов за производительность, связанных с явным стекем
Я смотрю в основном на первое, хотя второе и третье очень важны для этой проблемы и увеличит мое понимание Python.