language agnostic - Как ваш любимый язык справляется с глубокой рекурсией?
Это правда, но если это так же просто, как и все, почему Python не делает это для меня, чтобы мой код выглядел как можно проще? (Я говорю, что это не для того, чтобы захлопнуть разработчиков Python, а потому, что ответ объясняет проблему).
Оптимизации рекурсии присутствовали в функциональных языках, так как, например, в 14 веке или что-то в этом роде. Реализации Haskell, CAML, Lisp обычно преобразуют, по меньшей мере, хвостовые рекурсивные функции в итерации: вы в основном делаете это, определяя, что это возможно, т. Е. Что функция может быть перегруппирована так, чтобы вместо рекурсивного вызова не использовались локальные переменные, отличные от возвращаемого значения , Один трюк, чтобы сделать возможной работу над возвращаемым возвращаемым значением перед возвратом, заключается в том, чтобы ввести дополнительный параметр «аккумулятора». Говоря простыми словами, это означает, что работу можно эффективно выполнить по пути «вниз», а не по пути «вверх»: поиск вокруг «как сделать функцию хвост-рекурсивный» для деталей.
Фактические данные о том, чтобы превратить хвостовую рекурсивную функцию в цикл, в основном, являются джиггером с вашим соглашением о вызове, так что вы можете «выполнить вызов» просто путем настройки параметров и перехода к началу функции, не беспокоясь о том, чтобы сохранить все это что вы знаете, что никогда не будете использовать. В ассемблере вам не нужно сохранять регистры сохранения вызовов, если анализ потока данных говорит вам, что они не используются за пределами вызова, и то же самое происходит с чем-либо в стеке: вам не нужно перемещать указатель стека при вызове, если вы не возражаете, чтобы «ваш» бит стека был нацарапан следующей рекурсией / итерацией.
Вопреки тому, как вы перефразировали людей Python, преобразование общей рекурсивной функции в итерацию не является тривиальной: например, если она многорекурсивна, то при простом подходе вам все равно нужен стек.
Однако Memoization - полезный метод для произвольно рекурсивных функций, который вам может понравиться, если вы заинтересованы в возможных подходах. Это означает, что каждый раз, когда функция оценивается, вы вставляете результат в кеш. Чтобы использовать это для оптимизации рекурсии, в основном, если ваша рекурсивная функция считается «вниз», и вы ее мнимаете, вы можете ее итеративно оценить, добавив цикл, который подсчитывает «вверх», вычисляя каждое значение функции по очереди, пока вы не достигнете цель. Это использует очень мало пространства стека при условии, что кеш memo достаточно велик для хранения всех значений, которые вам понадобятся: например, если f (n) зависит от f (n-1), f (n-2) и f (n -3) вам нужно только пространство для 3 значений в кеше: по мере того, как вы поднимаетесь, вы можете удалять лестницу. Если f (n) зависит от f (n-1) и f (n / 2), вам нужно много места в кеше, но все равно меньше, чем вы использовали бы для стека в неоптимизированной рекурсии.
Недавно я начал изучать Python, и я был довольно удивлен, увидев предел 1000 глубоких рекурсий (по умолчанию). Если вы установите его достаточно высоким, около 30000, он падает с ошибкой сегментации, как и на C. Хотя, похоже, C выглядит намного выше.
(Люди Python быстро указывают, что вы всегда можете преобразовывать рекурсивные функции в итеративные и что они всегда быстрее. Это на 100% верно. На самом деле это не то, о чем мой вопрос.)
Я пробовал тот же эксперимент в Perl и где-то около 10 миллионов рекурсий он потреблял все мои 4 гигабайта, и я использовал ^ C, чтобы перестать пытаться. Очевидно, что Perl не использует стек C, но он использует смехотворный объем памяти, когда он рекурсирует - не ужасно шокирует, учитывая, сколько работы он должен выполнять для вызова функций.
Я попытался в Пайке и был полностью удивлен, получив 100 000 000 рекурсий примерно через 2 секунды. Я понятия не имею, как это произошло, но я подозреваю, что он сплющил рекурсию на итеративный процесс - он, похоже, не потребляет лишней памяти, пока он это делает. [Примечание: Пайк сглаживает тривиальные случаи, но segfaults на более сложных, или так мне говорят.]
Я использовал эти бесполезные функции:
Мне очень любопытно, как другие языки (например, PHP, Ruby, Java, Lua, Ocaml, Haskell) обрабатывают рекурсию и почему они так ее обрабатывают. Кроме того, обратите внимание, имеет ли значение, если функция «хвостовая рекурсия» (см. Комментарий).
C # /. NET будет использовать хвостовую рекурсию в определенном наборе обстоятельств. (Компилятор C # не генерирует код операции tailcall, но JIT в некоторых случаях реализует хвостовую рекурсию .
У Шри Борда также есть сообщение на эту тему . Конечно, CLR постоянно меняется, а с .NET 3.5 и 3.5SP1 он может снова измениться по отношению к хвостовым вызовам.
Running ruby 1.9.2dev (2010-07-11 версия 28618) [x86_64-darwin10.0.0] на старшей белой macbook:
выходы 9353 для меня, а это означает, что Ruby craps имеет менее 10000 вызовов в стеке.
С кросс-рекурсией, например:
он держится в половине случаев, при 4677 (
Я могу выжать еще несколько итераций, завернув рекурсивный вызов в proc:
который получает до 4850 перед ошибкой.
Visual Dataflex будет переполнять поток.
В некоторых непатологических случаях (например, ваш), (последний) Lua будет использовать рекурсию хвостового вызова , т.е. он просто прыгнет, не вдавливая данные в стек. Таким образом, количество циклов рекурсии может быть почти неограниченным.
Протестировано с помощью:
и даже попробовал кросс-рекурсию (f вызывая g и g, вызывающие f . ).
В Windows Lua 5.1 использует около 1,1 МБ (постоянный) для его запуска, заканчивается через несколько секунд.
Используя следующую команду в интерактивной консоли F #, она выполнялась менее чем за секунду:
Затем я попробовал прямой перевод, т.е.
Тот же результат, но другая компиляция.
Это выглядит так, как при переводе на C #:
g , однако переводится на это:
Интересно, что две функции, которые принципиально одинаковы, по-разному интерпретируются компилятором F #. Он также показывает, что компилятор F # имеет рекурсивную оптимизацию. Таким образом, это должно занять до тех пор, пока я не достигнет предела для 32-битных целых чисел.
Согласно этой теме, около 5 000 000 с java, 1Gb RAM. (и что с «клиентской» версией точки доступа)
Это было со стеком (-Xss) 300Mo.
С опцией -server это может быть увеличено.
Также можно попытаться оптимизировать компилятор (например, JET ), чтобы уменьшить накладные расходы на каждом уровне.
Существует способ улучшить код Perl , чтобы он использовал стек с постоянным размером. Вы делаете это, используя специальную форму goto .
При первом вызове он выделяет пространство в стеке. Затем он изменит свои аргументы и перезапустит подпрограмму, не добавляя ничего больше в стек. Поэтому он притворяется, что никогда не называл себя, меняя его на итеративный процесс.
Вы также можете использовать модуль Sub::Call::Recur . Это делает код более понятным и короче.
У PHP есть предел по умолчанию 100, прежде чем он умрет:
Fatal error: Maximum function nesting level of '100' reached, aborting!
Изменить: вы можете изменить предел с помощью ini_set('xdebug.max_nesting_level', 100000); , но если вы перейдете примерно на 1150 итераций, то сбой PHP:
[Fri Oct 24 11:39:41 2008] [notice] Parent: child process exited with status 3221225477 -- Restarting.
Это скорее вопрос реализации, чем вопрос на языке. Нет ничего, что помешало бы некоторому (stoopid) компилятору C-компилятора также ограничить их стек вызовов до 1000. Там есть много небольших процессоров, у которых не было бы стекового пространства даже для многих.
(Люди Python быстро указывают, что вы всегда можете преобразовывать рекурсивные функции в итеративные и что они всегда быстрее. Это на 100% верно. На самом деле это не то, о чем мой вопрос.)
Возможно, они это скажут, но это не совсем правильно. Рекурсия всегда может быть преобразована в итерацию, но иногда также требуется ручное использование стека . В этих обстоятельствах я мог видеть, что рекурсивная версия работает быстрее (предполагая, что вы достаточно умны, чтобы сделать простые оптимизации, например, вытягивание ненужных объявлений за пределы рекурсивной подпрограммы). В конце концов, стек вызывает вызовы окружающих процедур - это хорошо ограниченная проблема, которую ваш компилятор должен знать, как оптимизировать очень хорошо. С другой стороны, операции с ручным стеком не будут иметь специализированного кода оптимизации в вашем компиляторе и могут иметь всевозможные проверки работоспособности пользовательского интерфейса, которые потребуют дополнительных циклов.
Возможно, решение Ierative / stack всегда работает быстрее на Python . Если это так, это ошибка Python, а не рекурсии.
Я довольно поклонник функционального программирования, и поскольку большинство этих langauges реализуют оптимизацию хвостовых вызовов, вы можете реорганизовать столько, сколько хотите: -P
Однако практически, я должен использовать много Java и использовать Python слишком много. Не знаю, какой предел для Java существует, но для Python я фактически планировал (но еще не сделал этого) для реализации декоратора, который бы хвост вызывал оптимизацию декорированной функции. Я планировал, чтобы это не оптимизировало рекурсию, но в основном как упражнение по динамическому исправлению байт-кода Python и больше узнать о внутренних компонентах Pythons. Вот несколько ссылок: http://lambda-the-ultimate.org/node/1331 и http://www.rowehl.com/blog/?p=626
clojure обеспечивает особую форму для рекурсии хвоста «recur», это может быть использовано только в хвостовых частях ast. В противном случае он ведет себя как java и, скорее всего, выкинет исключение StackverflowException.