Я знаю, что рекурсия иногда намного чище, чем зацикливание, и я ничего не спрашиваю о том, когда мне следует использовать рекурсию поверх итерации, я знаю, что уже есть много вопросов по этому поводу.
Что я спрашиваю, так ли рекурсия всегда быстрее, чем цикл? Мне кажется, вы всегда сможете уточнить цикл и заставить его работать быстрее, чем рекурсивная функция, потому что цикл отсутствует, постоянно настраивая новые кадры стека.
Я специально ищу, является ли рекурсия быстрее в приложениях, где рекурсия является правильным способом обработки данных, например, в некоторых функциях сортировки, в двоичных деревьях и т. Д.
performance
loops
recursion
iteration
Карсон Майерс
источник
источник
Ответы:
Это зависит от используемого языка. Вы написали «независимый от языка», поэтому я приведу несколько примеров.
В Java, C и Python рекурсия довольно дорога по сравнению с итерацией (в целом), потому что она требует выделения нового фрейма стека. В некоторых C-компиляторах можно использовать флаг компилятора, чтобы устранить эти издержки, которые преобразуют определенные типы рекурсии (фактически, определенные типы хвостовых вызовов) в переходы вместо вызовов функций.
В реализациях на языке функционального программирования иногда итерация может быть очень дорогой, а рекурсия - очень дешевой. Во многих случаях рекурсия превращается в простой переход, но для изменения переменной цикла (которая является изменяемой) иногда требуются некоторые относительно тяжелые операции, особенно в реализациях, которые поддерживают несколько потоков выполнения. В некоторых из этих сред мутация обходится дорого из-за взаимодействия между мутатором и сборщиком мусора, если они оба могут работать одновременно.
Я знаю, что в некоторых реализациях Scheme рекурсия, как правило, будет быстрее, чем зацикливание.
Короче говоря, ответ зависит от кода и реализации. Используйте любой стиль, который вы предпочитаете. Если вы используете функциональный язык, рекурсия может быть быстрее. Если вы используете императивный язык, итерация, вероятно, быстрее. В некоторых средах оба метода приводят к созданию одной и той же сборки (поместите ее в свою трубу и выкурите ее).
Приложение: В некоторых средах лучшая альтернатива - это не рекурсия и не итерация, а функции высшего порядка. К ним относятся «карта», «фильтр» и «уменьшить» (что также называется «сложить»). Они не только являются предпочтительным стилем, они не только часто более чистые, но и в некоторых средах эти функции являются первыми (или единственными), которые получают преимущество от автоматического распараллеливания - поэтому они могут быть значительно быстрее, чем итерация или рекурсия. Data Parallel Haskell является примером такой среды.
Понимание списков - это еще одна альтернатива, но обычно это просто синтаксический сахар для функций итерации, рекурсии или функций более высокого порядка.
источник
Нет, Итерация всегда будет быстрее, чем Рекурсия. (в архитектуре фон Неймана)
Объяснение:
Если вы создаете минимальное количество операций с обычного компьютера с нуля, «Итерация» стоит на первом месте в качестве строительного блока и требует меньше ресурсов, чем «рекурсия», следовательно, это быстрее.
Создание псевдо-вычислительной машины с нуля:
Задайте себе вопрос : что вам нужно, чтобы вычислить значение, то есть следовать алгоритму и достичь результата?
Мы создадим иерархию понятий, начиная с нуля и определяя в первую очередь базовые, основные понятия, затем создадим понятия второго уровня с ними и так далее.
Первая концепция: ячейки памяти, память, состояние . Чтобы что-то сделать, вам нужны места для хранения конечных и промежуточных значений результатов. Давайте предположим, что у нас есть бесконечный массив «целочисленных» ячеек, называемый Memory , M [0..Infinite].
Инструкции: сделать что-то - трансформировать ячейку, изменить ее значение. изменить состояние . Каждая интересная инструкция выполняет преобразование. Основные инструкции:
а) Установить и переместить ячейки памяти
б) логика и арифметика
Исполнительный агент : ядро современного процессора. «Агент» - это то, что может выполнять инструкции. Агент также может быть людьми по алгоритму на бумаге.
Порядок шагов: последовательность инструкций : то есть: сделать это сначала, сделать это после и т. Д. Обязательная последовательность инструкций. Даже однострочные выражения являются «обязательной последовательностью инструкций». Если у вас есть выражение с определенным «порядком оценки», то у вас есть шаги . Это означает, что даже одно составное выражение имеет неявные «шаги», а также имеет неявную локальную переменную (назовем ее «результат»). например:
Выражение выше подразумевает 3 шага с неявной переменной «result».
Поэтому даже инфиксные выражения, поскольку у вас есть определенный порядок вычисления, являются обязательной последовательностью инструкций . Выражение подразумевает последовательность операций, которые должны быть выполнены в определенном порядке, и, поскольку существуют шаги , существует также неявная промежуточная переменная «result».
Указатель инструкций : если у вас есть последовательность шагов, у вас также есть неявный «указатель инструкций». Указатель инструкции отмечает следующую инструкцию и продвигается после чтения инструкции, но до ее выполнения.
В этой псевдо-вычислительной машине указатель инструкций является частью памяти . (Примечание: обычно указатель инструкций будет «специальным регистром» в ядре ЦП, но здесь мы упростим концепции и предположим, что все данные (включая регистры) являются частью «памяти»)
Переход - если у вас есть заказанное количество шагов и указатель инструкций , вы можете применить инструкцию « store », чтобы изменить значение самого указателя инструкций. Мы будем называть это конкретное использование инструкции store новым именем: Jump . Мы используем новое имя, потому что его проще воспринимать как новую концепцию. Изменяя указатель инструкции, мы инструктируем агента «перейти к шагу x».
Бесконечная итерация : отскочив назад, теперь вы можете заставить агента «повторить» определенное количество шагов. На данный момент у нас есть бесконечная итерация.
Условно - условное выполнение инструкций. С помощью условного предложения вы можете условно выполнить одну из нескольких инструкций в зависимости от текущего состояния (которое может быть установлено с помощью предыдущей инструкции).
Правильная итерация : теперь с помощью условного предложения мы можем избежать бесконечного цикла инструкции возврата . Теперь у нас есть условный цикл, а затем правильная итерация
Именование : присвоение имен определенной ячейке памяти, содержащей данные или шаг . Это просто «удобство». Мы не добавляем никаких новых инструкций, имея возможность определять «имена» для областей памяти. «Наименование» - это не инструкция для агента, а просто удобство для нас. Присвоение имен делает код (на данный момент) более простым для чтения и изменения.
Одноуровневая подпрограмма . Предположим, вам нужно выполнить серию шагов. Вы можете сохранить шаги в именованной позиции в памяти, а затем перейти к этой позиции, когда вам нужно выполнить их (вызвать). В конце последовательности вам нужно вернуться к точке вызова, чтобы продолжить выполнение. С помощью этого механизма вы создаете новые инструкции (подпрограммы), составляя основные инструкции.
Реализация: (новые концепции не требуются)
Проблема с одноуровневой реализацией: Вы не можете вызвать другую подпрограмму из подпрограммы. Если вы это сделаете, вы перезапишете возвращающий адрес (глобальную переменную), поэтому вы не сможете вкладывать вызовы.
Чтобы иметь лучшую реализацию для подпрограмм: вам нужен STACK
Стек : Вы определяете пространство памяти для работы в качестве «стека», вы можете «выталкивать» значения в стек, а также «выталкивать» последнее «вытолкнутое» значение. Для реализации стека вам понадобится указатель стека (подобный указателю инструкций), который указывает на фактическую «верхушку» стека. Когда вы «нажимаете» значение, указатель стека уменьшается, и вы сохраняете значение. Когда вы «выталкиваете», вы получаете значение в фактическом указателе стека, а затем увеличивается указатель стека.
Подпрограммы Теперь, когда у нас есть стек, мы можем реализовать надлежащие подпрограммы, разрешающие вложенные вызовы . Реализация аналогична, но вместо того, чтобы хранить указатель инструкций в предопределенной позиции памяти, мы «помещаем» значение IP в стек . В конце подпрограммы мы просто «выталкиваем» значение из стека, фактически возвращаясь к инструкции после исходного вызова . Эта реализация, имеющая «стек», позволяет вызывать подпрограмму из другой подпрограммы. С помощью этой реализации мы можем создать несколько уровней абстракции при определении новых инструкций в качестве подпрограмм, используя базовые инструкции или другие подпрограммы в качестве строительных блоков.
Рекурсия : что происходит, когда подпрограмма вызывает себя? Это называется "рекурсия".
Проблема: Перезаписывая локальные промежуточные результаты, подпрограмма может быть сохранена в памяти. Поскольку вы вызываете / повторно используете одни и те же шаги, если промежуточный результат хранится в предопределенных ячейках памяти (глобальных переменных), они будут перезаписаны при вложенных вызовах.
Решение: чтобы разрешить рекурсию, подпрограммы должны хранить локальные промежуточные результаты в стеке , поэтому при каждом рекурсивном вызове (прямом или косвенном) промежуточные результаты хранятся в разных местах памяти.
...
Достигнув рекурсии, мы останавливаемся здесь.
Вывод:
В архитектуре фон Неймана ясно, что «Итерация» является более простым / базовым понятием, чем «Рекурсия» . У нас есть форма «Итерации» на уровне 7, в то время как «Рекурсия» находится на уровне 14 иерархии понятий.
Итерация всегда будет быстрее в машинном коде, потому что она подразумевает меньше инструкций и, следовательно, меньше циклов ЦП.
Какой лучше"?
Вам следует использовать «итерацию», когда вы обрабатываете простые последовательные структуры данных, и везде будет работать «простой цикл».
Вы должны использовать «рекурсию», когда вам нужно обработать рекурсивную структуру данных (мне нравится называть их «фрактальными структурами данных»), или когда рекурсивное решение явно более «элегантно».
Совет : используйте лучший инструмент для работы, но понимайте внутреннюю работу каждого инструмента, чтобы выбирать мудро.
Наконец, обратите внимание, что у вас есть много возможностей использовать рекурсию. Повсюду у вас есть рекурсивные структуры данных , сейчас вы смотрите на них: части DOM, поддерживающие то, что вы читаете, - это RDS, выражение JSON - это RDS, иерархическая файловая система на вашем компьютере - это RDS, то есть: у вас есть корневой каталог, содержащий файлы и каталоги, каждый каталог, содержащий файлы и каталоги, каждый из этих каталогов, содержащий файлы и каталоги ...
источник
Рекурсия может быть быстрее, если альтернативой является явное управление стеком, как в алгоритмах сортировки или двоичного дерева, о которых вы упомянули.
У меня был случай, когда переписывание рекурсивного алгоритма в Java сделало его медленнее.
Таким образом, правильный подход заключается в том, чтобы сначала написать его наиболее естественным образом, оптимизировать только в том случае, если профилирование показывает его критичность, а затем измерить предполагаемое улучшение.
источник
Хвостовая рекурсия выполняется так же быстро, как и петля. Во многих функциональных языках реализована хвостовая рекурсия.
источник
Рассмотрим, что абсолютно необходимо сделать для каждой итерации и рекурсии.
Вы видите, что здесь не так много места для разногласий.
(Я предполагаю, что рекурсия - это хвостовой вызов, а компилятор знает об этой оптимизации).
источник
Большинство ответов здесь забывают очевидного виновника, почему рекурсия часто медленнее, чем итерационные решения. Это связано со сборкой и разборкой стековых фреймов, но не совсем так. Как правило, это большая разница в хранении автоматической переменной для каждой рекурсии. В итеративном алгоритме с циклом переменные часто хранятся в регистрах, и даже если они разливаются, они будут находиться в кэше уровня 1. В рекурсивном алгоритме все промежуточные состояния переменной хранятся в стеке, что означает, что они вызовут еще много разливов в память. Это означает, что даже если он выполняет одинаковое количество операций, у него будет много обращений к памяти в горячем цикле, и, что еще хуже, эти операции с памятью имеют паршивую частоту повторного использования, что делает кэши менее эффективными.
TL; DR рекурсивные алгоритмы обычно хуже кешируют, чем итеративные.
источник
Большинство ответов здесь неверны . Правильный ответ - это зависит . Например, вот две функции C, которые проходят по дереву. Сначала рекурсивный:
А вот та же функция, реализованная с использованием итерации:
Не важно понимать детали кода. Просто
p
это узлы, и этоP_FOR_EACH_CHILD
делает ходьбу. В итерационной версии нам нужен явный стек,st
в который узлы помещаются, а затем выталкиваются и обрабатываются.Рекурсивная функция выполняется намного быстрее, чем итеративная. Причина в том, что в последнем случае для каждого элемента требуется
CALL
функция a,st_push
а затем другая функцияst_pop
.В первом случае у вас есть только рекурсив
CALL
для каждого узла.Кроме того, доступ к переменным в стеке вызовов невероятно быстр. Это означает, что вы читаете из памяти, которая, вероятно, всегда будет в самом внутреннем кэше. С другой стороны, явный стек должен поддерживаться
malloc
: памятью из кучи, доступ к которой намного медленнее.С осторожной оптимизацией, такой как встраивание
st_push
иst_pop
, я могу достичь примерно паритета с рекурсивным подходом. Но по крайней мере на моем компьютере стоимость доступа к куче памяти больше, чем стоимость рекурсивного вызова.Но это обсуждение в основном спорный вопрос , потому что рекурсивное дерево ходьба неправильно . Если у вас достаточно большое дерево, вам не хватит места для стека вызовов, поэтому необходимо использовать итерационный алгоритм.
источник
В общем, нет, рекурсия не будет быстрее цикла при любом реалистичном использовании, которое имеет жизнеспособные реализации в обеих формах. Я имею в виду, конечно, вы могли бы кодировать циклы, которые занимают вечность, но были бы лучшие способы реализовать тот же цикл, который мог бы превзойти любую реализацию той же проблемы с помощью рекурсии.
Вы ударили гвоздь по голове относительно причины; Создание и уничтожение стековых фреймов обходится дороже, чем простой переход.
Тем не менее, обратите внимание, что я сказал «имеет жизнеспособные реализации в обеих формах». Для таких вещей, как многие алгоритмы сортировки, существует не очень жизнеспособный способ их реализации, который эффективно не устанавливает собственную версию стека из-за порождения дочерних «задач», которые по своей сути являются частью процесса. Таким образом, рекурсия может быть такой же быстрой, как и попытка реализовать алгоритм с помощью цикла.
Изменить: Этот ответ предполагает, что нефункциональные языки, где большинство основных типов данных являются изменяемыми. Это не относится к функциональным языкам.
источник
В любой реалистичной системе создание фрейма стека всегда будет обходиться дороже, чем INC и JMP. Вот почему действительно хорошие компиляторы автоматически преобразуют хвостовую рекурсию в вызов одного и того же фрейма, то есть без дополнительных затрат, поэтому вы получаете более читаемую исходную версию и более эффективную скомпилированную версию. Действительно, очень хороший компилятор должен даже быть в состоянии преобразовать нормальные рекурсии в хвост рекурсии , где это возможно.
источник
Функциональное программирование - это скорее « что », а не « как ».
Разработчики языка найдут способ оптимизировать работу кода под ним, если мы не попытаемся сделать его более оптимизированным, чем нужно. Рекурсию также можно оптимизировать на языках, которые поддерживают оптимизацию хвостовых вызовов.
Что важнее с точки зрения программиста, так это удобочитаемость и удобство обслуживания, а не оптимизация. Опять же, «преждевременная оптимизация - корень всего зла».
источник
Это предположение. Обычно рекурсия, вероятно, не справляется с цикличностью часто или когда-либо при проблемах приличного размера, если оба используют действительно хорошие алгоритмы (не считая сложности реализации), она может отличаться, если используется с рекурсией языка с хвостовой рекурсией (и хвостовым рекурсивным алгоритмом) и с циклами также как часть языка) - которые, вероятно, будут очень похожи и, возможно, даже предпочтут рекурсию иногда.
источник
Согласно теории это то же самое. Рекурсия и цикл с одинаковой сложностью O () будут работать с той же теоретической скоростью, но, конечно, реальная скорость зависит от языка, компилятора и процессора. Пример со степенью числа может быть итеративно закодирован с помощью O (ln (n)):
источник
O(n)
, но один может занятьx
больше времени, чем другой, для всехn
.