Подтвердить что ты не робот

Как ваш любимый язык справляется с глубокой рекурсией?

Недавно я начал изучать Python, и я был довольно удивлен, обнаружив предел 1000 глубоких рекурсий (по умолчанию). Если вы установите его достаточно высоким, около 30000, он выйдет из строя с ошибкой сегментации, как и C. Хотя, похоже, C выглядит намного выше.

(Люди из Python быстро указывают, что вы всегда можете преобразовывать рекурсивные функции в итеративные и что они всегда быстрее. Это 100% истинно. На самом деле это не совсем мой вопрос.)

Я пробовал тот же эксперимент в Perl и где-то около 10 миллионов рекурсий он потреблял все мои 4 гигабайта, и я использовал ^ C, чтобы перестать пытаться. Ясно, что Perl не использует стек C, но он использует смехотворный объем памяти, когда он рекурсирует - не ужасно шокирует, учитывая, сколько работы он должен выполнять для вызова функций.

Я попытался в Пайке и был полностью удивлен, получив 100 000 000 рекурсий примерно через 2 секунды. Я понятия не имею, как это произошло, но я подозреваю, что он сплющил рекурсию на итеративный процесс - он, похоже, не потребляет лишней памяти, пока он это делает. [Примечание: Пайк сглаживает тривиальные случаи, но segfaults на более сложных, или так мне говорят.]

Я использовал эти бесполезные функции:

int f(int i, int l) { if(i<l) return f(i+1,l); return i; }

sub f { return f($_[0]+1, $_[1]) if $_[0]<$_[1]; return $_[0] };

def f(i,l):
   if i<l:
     return f(i+1,l)
   return i

Мне очень любопытно, как другие языки (например, PHP, Ruby, Java, Lua, Ocaml, Haskell) обрабатывают рекурсию и почему они так ее обрабатывают. Кроме того, обратите внимание, не имеет значения, является ли функция "хвостовой рекурсивной" (см. Комментарий).

4b9b3361

Ответ 1

"Люди Python быстро указывают, что вы всегда можете преобразовывать рекурсивные функции в итеративные и что они всегда быстрее"

Это правда, но если это так же просто, как и все, почему 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), вам нужно много места в кеше, но все же меньше, чем вы использовали бы для стека в неоптимизированной рекурсии.

Ответ 2

Это скорее вопрос реализации, чем вопрос на языке. Там ничто не останавливает некоторого (stoopid) компилятора C-компилятора, также ограничивая их стек вызовов до 1000. Существует много небольших процессоров, у которых не было бы пространства стека даже для многих.

(Люди из Python быстро указывают, что вы всегда можете преобразовывать рекурсивные функции в итеративные и что они всегда быстрее. Это 100% истинно. На самом деле это не совсем мой вопрос.)

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

Возможно, решение Ierative/stack всегда работает быстрее в Python. Если это так, это отказ от Python, а не рекурсии.

Ответ 3

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.

Ответ 4

С#/.NET будет использовать хвостовую рекурсию в определенном наборе обстоятельств. (Компилятор С# не генерирует код операции tailcall, но JIT будет реализовывать хвостовую рекурсию в некоторых случаях.

Shri Borde также имеет сообщение в этом разделе. Конечно, CLR постоянно меняется, а с .NET 3.5 и 3.5SP1 он может снова измениться по отношению к хвостовым вызовам.

Ответ 5

Используя следующую команду в интерактивной консоли F #, она заняла менее секунды:

let rec f i l = 
  match i with 
  | i when i < l -> f (i+1) l
  | _ -> l

f 0 100000000;;

Затем я попробовал прямой перевод, т.е.

let rec g i l = if i < l then g (i+1) l else l

g 0 100000000;;

Тот же результат, но другая компиляция.

Это то, что f выглядит, когда переводится на С#:

int f(int i, int l)
{
  while(true)
  {
    int num = i;
    if(num >= l)
      return l;
    int i = num;
    l = l;
    i = i + 1;
  }
}

g, однако переводится на это:

int g(int i, int l)
{
  while(i < l)
  {
    l = l;
    i++;
  }
  return l;
}

Интересно, что две функции, которые принципиально одинаковы, по-разному интерпретируются компилятором F #. Он также показывает, что компилятор F # имеет рекурсивную оптимизацию. Таким образом, это должно зацикливаться до тех пор, пока я не достигнет предела для 32-битных целых чисел.

Ответ 6

В соответствии с этим потоком около 5 000 000 с java, 1 Гб оперативной памяти. (и что, с "клиентской" версией точки доступа)

Это было с stack (-Xss) из 300Mo.

С опцией -сервера это может быть увеличено.

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

Ответ 7

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

Протестировано с помощью

function f(i, l)
    if i < l then
        return f(i+1, l)
    end
    return i
end

local val1  = arg[1] or 1
local val2  = arg[2] or 100000000
print(f(val1 + 0, val2 + 0))

Также с помощью:

function g(i, l)
    if i >= l then
        return i
    end
    return g(i+1, l)
end

и даже попробовал кросс-рекурсию (f вызов g и g вызов f...).

В Windows Lua 5.1 использует около 1,1 МБ (константа) для его запуска, заканчивается через несколько секунд.

Ответ 8

Запуск ruby ​​1.9.2dev (2010-07-11 версия 28618) [x86_64-darwin10.0.0] на старшей белой macbook:

def f
  @i += 1
  f
end

@i = 0

begin
  f
rescue SystemStackError
  puts @i
end

выводит 9353 для меня, что означает, что Ruby craps имеет менее 10000 вызовов в стеке.

С кросс-рекурсией, например:

def f
  @i += 1
  g
end

def g
  f
end

он держится за половину времени, при 4677 (~ = 9353/2).

Я могу выжать еще несколько итераций, обернув рекурсивный вызов в proc:

def f
  @i += 1
  yield
end

@i = 0
@block = lambda { f(&@block) }

begin
  f(&@block)
rescue SystemStackError
  puts @i
end

который получает до 4850 перед ошибкой.

Ответ 9

В Visual Dataflex произойдет переполнение стека.

Ответ 10

Существует способ улучшить код Perl, чтобы он использовал стек с постоянным размером. Вы делаете это, используя специальную форму goto.

sub f{
  if( $_[0] < $_[1] ){

    # return f( $_[0]+1, $_[1] );

    @_ = ( $_[0]+1, $_[1] );
    goto &f;

  } else {
    return $_[0]
  }
}

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


Вы также можете использовать модуль Sub::Call::Recur. Это делает код более понятным и короче.

use Sub::Call::Recur;
sub f{
  recur( $_[0]+1, $_[1] ) if $_[0] < $_[1];
  return $_[0];
}

Ответ 11

Я являюсь поклонником функционального программирования, и поскольку большинство этих langauges реализуют оптимизацию хвостового вызова, вы можете реорганизовать столько, сколько хотите: -P

Однако, практически, я должен использовать много Java и много использовать Python. Не знаю, какой предел для Java существует, но для Python я фактически планировал (но еще не сделал этого) для реализации декоратора, который бы хвост вызывал оптимизацию декорированной функции. Я планировал, чтобы это не оптимизировало рекурсию, но в основном как упражнение по динамическому исправлению байт-кода Python и больше узнать о внутренних компонентах Pythons. Вот несколько ссылок: http://lambda-the-ultimate.org/node/1331 и http://www.rowehl.com/blog/?p=626

Ответ 12

clojure предоставляет специальную форму для рекурсии хвоста "recur", это можно использовать только в хвостовых частях ast. В противном случае он ведет себя как java и, скорее всего, выдает исключение StackverflowException.