Рекурсия когда-либо быстрее, чем цикл?

Я знаю, что рекурсия иногда намного чище, чем цикл, и я ничего не спрашиваю о том, когда я должен использовать рекурсию по итерации, я знаю, что уже есть много вопросов об этом.

то, что я спрашиваю, это рекурсия когда-нибудь быстрее, чем цикл? Мне кажется, что вы всегда сможете уточнить цикл и заставить его работать быстрее, чем рекурсивная функция, потому что цикл отсутствует, постоянно настраивая новый стек кадры.

Я специально ищу, быстрее ли рекурсия в приложениях, где рекурсия является правильным способом обработки данных, например, в некоторых функциях сортировки, в двоичных деревьях и т. д.

12 ответов


Это зависит от используемого языка. Вы написали "язык-агностик", поэтому я приведу несколько примеров.

в Java, C и Python рекурсия довольно дорога по сравнению с итерацией (в целом), потому что она требует выделения нового кадра стека. В некоторых компиляторах C можно использовать флаг компилятора для устранения этих накладных расходов, который преобразует определенные типы рекурсии (фактически, определенные типы хвостовых вызовов) в Прыжки вместо вызовов функций.

In реализации функционального языка программирования, иногда итерация может быть очень дорогой, а рекурсия может быть очень дешевой. Во многих рекурсия преобразуется в простой прыжок, но изменение переменной цикла (которая является изменяемой) иногда требует некоторых относительно тяжелых операций, особенно на реализациях, которые поддерживают несколько потоков выполнения. Мутация стоит дорого в некоторых из этих сред из-за взаимодействия между мутатором и мусором коллектор, если оба могут работать одновременно.

Я знаю, что в некоторых реализациях схема рекурсия обычно будет быстрее, чем цикл.

короче, ответ зависит от кода и реализации. Использовать любой стиль, который вы предпочитаете. Если вы используете функциональный язык, рекурсия может будет быстрее. Если вы используете императивный язык, итерация наверное быстрее. В некоторых средах оба метода приведут к та же сборка генерируется (поместите это в свою трубку и курите ее).

дополнение: в некоторых средах лучшей альтернативой является не рекурсия и не итерация, а функции более высокого порядка. К ним относятся "карта", "фильтр" и "уменьшить"(который также называется "сгиб"). Они не только являются предпочтительным стилем, не только они часто чище, но в некоторых средах эти функции являются первыми ( или только), чтобы получить импульс от автоматического распараллеливания-так они могут быть значительно быстрее, чем итерация или рекурсия. Примером такой среды является Data Parallel Haskell.

понимание списка-еще одна альтернатива, но обычно это просто синтаксический сахар для итерации, рекурсии или функций более высокого порядка.


рекурсия когда-либо быстрее, чем цикл?

нет, итерация всегда будет быстрее, чем рекурсия. (в архитектуре фон Неймана)

объяснение:

если вы создадите минимальные операции универсального компьютера с нуля, " итерация "будет сначала как строительный блок и будет менее ресурсоемкой, чем" рекурсия", ergo будет быстрее.

построение псевдо-вычислительной машины с нуля:

вопрос: что вам нужно вычислить значение, т. е. следовать алгоритму и достичь результата?

мы установим иерархию понятий, начиная с нуля и определяя в первую очередь основные, Основные понятия, затем построим концепции второго уровня с ними и так далее.

  1. Первая Концепция: ячейки памяти, хранение, состояние. Делать то, что вы нужно мест для хранения конечных и промежуточных результирующих значений. Предположим, у нас есть бесконечный массив "целочисленных" ячеек, называемых , M[0..Бесконечный.]

  2. инструкции: сделайте что-нибудь-преобразуйте ячейку, измените ее значение. изменить состояние. Каждая интересная инструкция выполняет преобразование. Основные инструкции:

    a) установить и переместить ячейки памяти

    • сохранить значение в памяти, например: магазин 5 м[4]
    • скопируйте значение в другую позицию: например:магазин m[4] m[8]

    b)логика и арифметика

    • и, или, xor, не
    • add, sub, mul, div. например,добавить m[7] m[8]
  3. An Агент-Исполнитель: a базовый в современном процессоре. "Агент" - это то, что может выполнять команды. Ан агент также может быть человеком, следующим алгоритму на бумаге.

  4. порядок шагов: последовательность инструкций: т. е.: сделайте это сначала, сделайте это после и т. д. Императивная последовательность инструкций. Даже одну строчку!--11-->выражения являются "императивной последовательностью инструкций". Если у вас есть выражение с определенным "порядком оценки" у вас есть шаги. Это означает, что даже одно составленное выражение имеет неявные "шаги", а также имеет неявную локальную переменную (назовем ее"результат"). например:

    4 + 3 * 2 - 5
    (- (+ (* 3 2) 4 ) 5)
    (sub (add (mul 3 2) 4 ) 5)  
    

    приведенное выше выражение подразумевает 3 шага с неявной переменной "результат".

    // pseudocode
    
           1. result = (mul 3 2)
           2. result = (add 4 result)
           3. result = (sub result 5)
    

    поэтому даже выражения infix, поскольку у вас есть определенный порядок оценки, являются императивная последовательность инструкций. Выражение подразумевает последовательность операций, которые должны быть сделаны в определенном порядке, и потому что есть шаги, существует также неявная промежуточная переменная "результат".

  5. Инструкция Указатель: если у вас есть последовательность шагов, у вас также есть неявный "указатель инструкции". Указатель инструкции отмечает следующую инструкцию и продвигается после чтения инструкции, но до выполнения инструкции.

    в этой псевдо-вычислительной машине указатель инструкции является частью . (Примечание: обычно Инструкция Указатель будет "специальный регистр" в ядре процессора, но здесь мы упростим понятия и предположим, что все данные (включая регистры) являются частью "памяти")

  6. прыжок - после того, как у вас есть заказанное количество шагов и Инструкция Указатель можно применить "магазине" инструкция для изменения значения самого указателя инструкции. Мы будем называть это конкретное использование магазин инструкция С новым именем: прыжок. Мы используем новое имя, потому что легче думать о нем как о новой концепции. Изменяя указатель инструкции, мы инструктируем агента "перейти к шагу x".

  7. Бесконечные Итерации: By прыжки назад, теперь вы может заставить агента "повторить" определенное количество шагов. На данный момент у нас есть бесконечные итерации.

                       1. mov 1000 m[30]
                       2. sub m[30] 1
                       3. jmp-to 2  // infinite loop
    
  8. условный - условное выполнение инструкций. С помощью предложения "conditional" вы можете условно выполнить одну из нескольких инструкций на основе текущего состояния (которое может быть установлено с предыдущей инструкцией).

  9. Правильный Шаг: теперь с условный предложения, мы можем избежать бесконечного цикла прыжок назад инструкция. Теперь у нас есть условный цикл а то правильный шаг

    1. mov 1000 m[30]
    2. sub m[30] 1
    3. (if not-zero) jump 2  // jump only if the previous 
                            // sub instruction did not result in 0
    
    // this loop will be repeated 1000 times
    // here we have proper ***iteration***, a conditional loop.
    
  10. наименования: давать имена к определенному положению памяти держа данные или держа шаг. Это просто "удобство". Мы не добавляем никаких новых инструкций, имея возможность определить "имена" для участков памяти. "Именование" не является инструкцией для агента, это просто удобство для нас. наименования делает код (на данный момент) легче читать и легче изменить.

       #define counter m[30]   // name a memory location
       mov 1000 counter
    loop:                      // name a instruction pointer location
        sub counter 1
        (if not-zero) jmp-to loop  
    
  11. одноуровневые подпрограммы: предположим, есть ряд шагов, которые необходимо выполнять часто. Вы можете сохранить шаги в именованной позиции в памяти, а затем перейти к это положение, когда вам нужно выполнить их (вызов). В конце последовательность вам нужно возвращение до вызов продолжить выполнение. С помощью этого механизма, вы создание новой инструкции (подпрограммы), составляя основные инструкции.

    реализация: (не требуется никаких новых концепций)

    • сохранить текущий указатель инструкции в предопределенной позиции памяти
    • прыжок функции
    • в конце из подпрограммы вы извлекаете указатель инструкции из предопределенного местоположения памяти, эффективно возвращаясь к следующей инструкции исходного вызов
    : перезапись локальных промежуточных результатов подпрограмма может храниться в памяти. Поскольку вы вызываете / повторно используете те же шаги,если промежуточный результат хранится в предопределенных местах памяти (глобальных переменных), они будут перезаписаны на вложенных вызовах.

    устранение: чтобы разрешить рекурсию, подпрограммы должны хранить локальные промежуточные результаты в стеке, следовательно, на каждого рекурсивный вызов (прямые или косвенные) промежуточные результаты хранятся в разных ячейках памяти.

...

достиг рекурсия мы останавливаемся здесь.

вывод:

в архитектуре фон Неймана, ясно "итерации" - это проще/базовое понятие, чем "рекурсия". У нас есть форма "итерации" на уровне 7, в то время как "рекурсия" находится на уровне 14 иерархии понятий.

шаг всегда будет быстрее в машинном коде, потому что это подразумевает меньше инструкций, поэтому меньше циклов процессора.

какой из них "лучше"?

  • вы должны использовать "итерацию", когда вы обрабатываете простой, последовательные структуры данных, и везде "простой цикл" будет делать.

  • вы должны использовать "рекурсию", когда вам нужно обработать рекурсивную структуру данных (мне нравится называть их" фрактальными структурами данных"), или когда рекурсивное решение явно более"элегантное".

советы: используйте лучший инструмент для работы, но понять внутреннюю работу каждого инструмента, чтобы выбрать мудро.

наконец, обратите внимание, что у вас есть множество возможностей использовать рекурсию. У вас есть Рекурсивные Структуры Данных!--12--> везде, вы смотрите на один Сейчас: части DOM, поддерживающие то, что Вы читаете, - это RDS, выражение JSON-это RDS, иерархическая файловая система на вашем компьютере-это RDS, i.e: у вас есть корневой каталог, содержащий файлы и каталоги, каждый каталог, содержащий файлы и каталоги, каждый из этих каталогов, содержащий файлы и каталоги...


рекурсия может быть быстрее, если альтернативой является явное управление стеком, как в алгоритмах сортировки или двоичного дерева, которые вы упомянули.

У меня был случай, когда переписывание рекурсивного алгоритма на Java сделало его медленнее.

поэтому правильный подход заключается в том, чтобы сначала написать его самым естественным образом, только оптимизировать, если профилирование показывает, что это критично, а затем измерить предполагаемое улучшение.


хвостовая рекурсия так же быстро, как зацикливание. Во многих функциональных языках реализована хвостовая рекурсия.


рассмотрим, что абсолютно необходимо сделать для каждого, итерации и рекурсии.

  • итерация: переход к началу цикла
  • рекурсия: переход к началу вызываемой функции

вы видите, что здесь не так много места для различий.

(Я предполагаю, что рекурсия является хвостовым вызовом и компилятор знает об этой оптимизации).


большинство ответов здесь забывают очевидного виновника, почему рекурсия часто медленнее, чем итеративные решения. Это связано с наращиванием и сносом кадров стека, но это не совсем так. Как правило, это большая разница в хранении автоматической переменной для каждой рекурсии. В итерационном алгоритме с циклом переменные часто хранятся в регистрах, и даже если они разливаются, они будут находиться в кэше уровня 1. В рекурсивном алгоритме, все промежуточные состояния переменной хранится в стеке, что означает, что они породят еще много разливов в память. Это означает, что даже если он делает такое же количество операций, у него будет много доступа к памяти в горячем цикле, и что делает его хуже, эти операции памяти имеют паршивую скорость повторного использования, что делает кэши менее эффективными.

TL;DR рекурсивные алгоритмы обычно имеют худшее поведение кэша, чем итеративные.


большинство ответов здесь неправильно. Правильный ответ:зависит. Например, вот две функции C, которые проходят через дерево. Сначала рекурсивный:

static
void mm_scan_black(mm_rc *m, ptr p) {
    SET_COL(p, COL_BLACK);
    P_FOR_EACH_CHILD(p, {
        INC_RC(p_child);
        if (GET_COL(p_child) != COL_BLACK) {
            mm_scan_black(m, p_child);
        }
    });
}

и вот та же функция, реализованная с помощью итерации:

static
void mm_scan_black(mm_rc *m, ptr p) {
    stack *st = m->black_stack;
    SET_COL(p, COL_BLACK);
    st_push(st, p);
    while (st->used != 0) {
        p = st_pop(st);
        P_FOR_EACH_CHILD(p, {
            INC_RC(p_child);
            if (GET_COL(p_child) != COL_BLACK) {
                SET_COL(p_child, COL_BLACK);
                st_push(st, p_child);
            }
        });
    }
}

не важно понимать детали кода. Вот именно!--2--> узлы и P_FOR_EACH_CHILD не ходить. В итеративной версии нам нужен явный стек st на какие узлы нажимаются, а затем выскакивают и манипулируют.

рекурсивная функция работает намного быстрее, чем итеративная. Причина в том, что в последнем для каждого элемента a CALL функции st_push требуется, а затем еще один st_pop.

в первом случае у вас есть только рекурсивные CALL для каждого узла.

кроме того, доступ к переменным в callstack невероятно быстрый. Это означает, что Вы читаете по памяти, которая вероятно, всегда будет в самом сокровенном тайнике. С другой стороны, явный стек должен поддерживаться malloc:ed память из кучи, которая намного медленнее для доступа.

С тщательной оптимизацией, такой как inlining st_push и st_pop, Я могу достичь примерно паритета с рекурсивным подходом. Но, по крайней мере на моем компьютере, стоимость доступа к памяти больше, чем стоимость рекурсивный вызов.

но это обсуждение в основном спорно, потому что рекурсивное дерево ходьба-это неправильно. Если у вас достаточно большое дерево, у вас закончится пространство callstack, поэтому необходимо использовать итерационный алгоритм.


в любой реалистичной системе, нет, создание кадра стека всегда будет дороже, чем INC и JMP. Вот почему действительно хорошие компиляторы автоматически преобразуют хвостовую рекурсию в вызов того же фрейма, т. е. без накладных расходов, поэтому вы получаете более читаемую исходную версию и более эффективную скомпилированную версию. А действительно хороший компилятор должен даже быть в состоянии превратить обычную рекурсию в хвостовую рекурсию, где это возможно.


функциональное программирование больше о"что" вместо "как".

языковые разработчики найдут способ оптимизировать работу кода, если мы не попытаемся сделать его более оптимизированным, чем это необходимо. Рекурсия также может быть оптимизирована в языках, поддерживающих оптимизацию хвостовых вызовов.

но, что более важно с точки зрения программиста-это читаемость и ремонтопригодность, а не оптимизация в первую место. Опять же, "преждевременная оптимизация-это корень всех зол".


Это догадка. Обычно рекурсия, вероятно , не бьет цикл часто или когда-либо по проблемам приличного размера, если оба используют действительно хорошие алгоритмы(не считая сложности реализации), это может быть по-другому, если используется с языком w/ рекурсия хвостового вызова(и хвост рекурсивный алгоритм и с циклами также как часть языка) - который, вероятно, будет очень похож и, возможно, даже предпочтет рекурсию некоторое время.


В общем, нет, рекурсия не будет быстрее цикла в любом реалистичном использовании, которое имеет жизнеспособные реализации в обеих формах. Я имею в виду, конечно, вы можете кодировать циклы, которые занимают вечность, но были бы лучшие способы реализовать тот же цикл, который может превзойти любую реализацию той же проблемы с помощью рекурсии.

вы попали в гвоздь на голове относительно причины; создание и уничтожение кадров стека дороже, чем простой прыжок.

, обратите внимание, что я сказал: "имеет жизнеспособные реализации в обеих формах". Для таких вещей, как многие алгоритмы сортировки, обычно не существует очень жизнеспособного способа их реализации, который не эффективно настраивает свою собственную версию стека из-за появления дочерних "задач", которые по своей сути являются частью процесса. Таким образом, рекурсия может быть такой же быстрой, как попытка реализовать алгоритм с помощью цикла.

Edit: этот ответ предполагает нефункциональные языки, где большинство основных типов данных изменчивый. Это не относится к функциональным языкам.


согласно теории это то же самое. Рекурсия и цикл с той же сложностью O() будут работать с той же теоретической скоростью, но, конечно, реальная скорость зависит от языка, компилятора и процессора. Пример с мощностью числа может быть закодирован итерационным способом с помощью O (ln (n)):

  int power(int t, int k) {
  int res = 1;
  while (k) {
    if (k & 1) res *= t;
    t *= t;
    k >>= 1;
  }
  return res;
  }