Переполнение стека из глубокой рекурсии в Java? - PullRequest
51 голосов
/ 14 мая 2009

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

Есть ли способ увеличить стек вызовов? Например, я могу сделать функции, которые имеют миллионы вызовов, как в Erlang?

Я замечаю это все больше и больше, когда сталкиваюсь с проблемами Project Euler.

Спасибо.

Ответы [ 10 ]

86 голосов
/ 14 мая 2009

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

Красная таблетка в руке? Хорошо, сюда, пожалуйста.

Есть способы, которыми вы можете обменять стек на кучу. Например, вместо того, чтобы делать рекурсивный вызов внутри функции, пусть она возвращает ленивую структуру данных , которая выполняет вызов при оценке. Затем вы можете раскрутить «стек» с помощью for-конструкции Java. Я продемонстрирую на примере. Считайте этот код Хаскеля:

map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = (f x) : map f xs

Обратите внимание, что эта функция никогда не оценивает хвост списка. Таким образом, функция на самом деле не должна делать рекурсивный вызов. В Haskell он на самом деле возвращает thunk для хвоста, который вызывается, если он когда-либо нужен. Мы можем сделать то же самое в Java (для этого используются классы из Функциональная Java ):

public <B> Stream<B> map(final F<A, B> f, final Stream<A> as)
  {return as.isEmpty()
     ? nil()
     : cons(f.f(as.head()), new P1<Stream<A>>()
         {public Stream<A> _1()
           {return map(f, as.tail);}});}

Обратите внимание, что Stream<A> состоит из значения типа A и значения типа P1, которое похоже на thunk, который возвращает остальную часть потока при вызове _1 (). Хотя это, конечно, выглядит как рекурсия, рекурсивный вызов map не выполняется, но становится частью структуры данных Stream.

Затем его можно развернуть с помощью обычной for-конструкции.

for (Stream<B> b = bs; b.isNotEmpty(); b = b.tail()._1())
  {System.out.println(b.head());}

Вот еще один пример, так как вы говорили о Project Euler. Эта программа использует взаимно рекурсивные функции и не использует стек даже для миллионов вызовов:

import fj.*; import fj.data.Natural;
import static fj.data.Enumerator.naturalEnumerator;
import static fj.data.Natural.*; import static fj.pre.Ord.naturalOrd;
import fj.data.Stream; import fj.data.vector.V2;
import static fj.data.Stream.*; import static fj.pre.Show.*;

public class Primes
  {public static Stream<Natural> primes()
    {return cons(natural(2).some(), new P1<Stream<Natural>>()
       {public Stream<Natural> _1()
         {return forever(naturalEnumerator, natural(3).some(), 2)
                 .filter(new F<Natural, Boolean>()
                   {public Boolean f(final Natural n)
                      {return primeFactors(n).length() == 1;}});}});}

   public static Stream<Natural> primeFactors(final Natural n)
     {return factor(n, natural(2).some(), primes().tail());}

   public static Stream<Natural> factor(final Natural n, final Natural p,
                                        final P1<Stream<Natural>> ps)
     {for (Stream<Natural> ns = cons(p, ps); true; ns = ns.tail()._1())
          {final Natural h = ns.head();
           final P1<Stream<Natural>> t = ns.tail();
           if (naturalOrd.isGreaterThan(h.multiply(h), n))
              return single(n);
           else {final V2<Natural> dm = n.divmod(h);
                 if (naturalOrd.eq(dm._2(), ZERO))
                    return cons(h, new P1<Stream<Natural>>()
                      {public Stream<Natural> _1()
                        {return factor(dm._1(), h, t);}});}}}

   public static void main(final String[] a)
     {streamShow(naturalShow).println(primes().takeWhile
       (naturalOrd.isLessThan(natural(Long.valueOf(a[0])).some())));}}

Еще одна вещь, которую вы можете сделать, чтобы обменять стек на кучу, - это использовать несколько потоков . Идея состоит в том, что вместо рекурсивного вызова вы создаете thunk, который делает вызов, передаете этот thunk новому потоку и позволяете текущему потоку выйти из функции. Это идея, лежащая в основе Stackless Python.

Ниже приведен пример этого на Java. Извините, что это немного непрозрачно, чтобы посмотреть без предложений import static:

public static <A, B> Promise<B> foldRight(final Strategy<Unit> s,
                                          final F<A, F<B, B>> f,
                                          final B b,
                                          final List<A> as)
  {return as.isEmpty()
     ? promise(s, P.p(b))
     : liftM2(f).f
         (promise(s, P.p(as.head()))).f
         (join(s, new P1<Promise<B>>>()
            {public Promise<B> _1()
              {return foldRight(s, f, b, as.tail());}}));}

Strategy<Unit> s поддерживается пулом потоков, а функция promise передает поток в пул потоков, возвращая Promise, что очень похоже на java.util.concurrent.Future, только лучше. См. Здесь. Дело в том, что описанный выше метод сворачивает праворекурсивную структуру данных вправо в стек O (1) , что обычно требует устранения хвостового вызова. Таким образом, мы эффективно достигли ТВК в обмен на некоторую сложность. Вы бы назвали эту функцию следующим образом:

Strategy<Unit> s = Strategy.simpleThreadStrategy();
int x = foldRight(s, Integers.add, List.nil(), range(1, 10000)).claim();
System.out.println(x); // 49995000

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

Еще одна вещь, которую вы можете сделать, - это использовать технику под названием trampolining . Батут - это вычисление, ограниченное структурой данных, через которое можно пройти. Функциональная библиотека Java включает в себя написанный мной тип данных Trampoline, который позволяет эффективно превращать любой вызов функции в оконечный вызов. Например, - это батут foldRightC, который складывается вправо в постоянном стеке:

public final <B> Trampoline<B> foldRightC(final F2<A, B, B> f, final B b)
  {return Trampoline.suspend(new P1<Trampoline<B>>()
    {public Trampoline<B> _1()
      {return isEmpty()
         ? Trampoline.pure(b)
         : tail().foldRightC(f, b).map(f.f(head()));}});}

Это тот же принцип, что и использование нескольких потоков, за исключением того, что вместо вызова каждого шага в своем собственном потоке мы создаем каждый шаг в куче, очень похоже на использование Stream, а затем запускаем все шаги в один цикл с Trampoline.run.

40 голосов
/ 14 мая 2009

Я думаю, вы могли бы использовать эти параметры

-ss Stacksize для увеличения нативного размер стека или

-oss Stacksize для увеличения Java размер стопки,

Стандартный размер стека по умолчанию составляет 128 КБ, с минимальным значением 1000 байтов. Размер стека Java по умолчанию составляет 400 КБ, с минимальным значением 1000 байтов.

http://edocs.bea.com/wls/docs61/faq/java.html#251197

EDIT:

После прочтения первого комментария (Чака), а также перечитывания вопроса и прочтения других ответов я хотел бы уточнить, что я интерпретировал вопрос как просто «увеличение размера стека». Я не собирался говорить, что вы можете иметь бесконечные стеки, например, в функциональном программировании (парадигма программирования, которая только поцарапала его поверхность).

23 голосов
/ 14 мая 2009

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

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

9 голосов
/ 14 мая 2009

Если вам нужно спросить, вы, вероятно, делаете что-то не так .

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

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

8 голосов
/ 14 мая 2009

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

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

7 голосов
/ 14 мая 2009

Вы можете установить это в командной строке:

java -Xss8M класс

6 голосов
/ 14 мая 2009

Clojure, который работает на Java VM, очень хотел бы реализовать оптимизацию хвостового вызова, но не может из-за ограничения в байт-коде JVM (я не знаю деталей). Как следствие, он может помочь только с помощью специальной формы "recur", которая реализует несколько основных функций, которые вы ожидаете от правильной хвостовой рекурсии.

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

1 голос
/ 13 января 2011
public static <A, B> Promise<B> foldRight(final Strategy<Unit> s,
                                          final F<A, F<B, B>> f,
                                          final B b,
                                          final List<A> as)
{
    return as.isEmpty() ? promise(s, P.p(b))
    : liftM2(f).f(promise(s, P.p(as.head())))
      .f(join(s, new F<List<A>, P1<Promise<B>>>()
        {
             public Promise<B> f(List<A> l)
             {
                 return foldRight(s, f, b, l);
             }
         }.f(as.tail())));
}
0 голосов
/ 27 ноября 2013

Я столкнулся с той же проблемой и в итоге переписал рекурсию в цикл for , и это помогло.

0 голосов
/ 02 марта 2012

в затмении, если вы используете, установите -xss2m в качестве аргументов vm.

или

-xss2m непосредственно в командной строке.

java -xss2m classname
...