web-dev-qa-db-fra.com

La pile déborde d'une récursion profonde en Java?

Après quelques expériences avec les langages fonctionnels, je commence à utiliser davantage la récursivité en Java - mais le langage semble avoir une pile d'appels relativement peu profonde d'environ 1 000.

Y a-t-il un moyen de rendre la pile d'appels plus grande? Comme puis-je créer des fonctions qui comptent des millions d’appels, comme dans Erlang?

Je le remarque de plus en plus quand je fais des problèmes avec Project Euler.

Merci.

51
Lucky

Je suppose que vous pourriez utiliser ces paramètres

-ss Stacksize pour augmenter le natif taille de la pile ou 

-oss Stacksize pour augmenter le Java taille de la pile,

La taille de pile native par défaut est 128k, avec une valeur minimale de 1000 octets . La taille de pile Java par défaut est 400k, avec une valeur minimale de 1000 octets.

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

MODIFIER:

Après avoir lu le premier commentaire (Chuck), ainsi que relu la question et lu une autre réponse, je voudrais préciser que j’ai interprété la question simplement comme "augmenter la taille de la pile". Je n'avais pas l'intention de dire que vous pouvez avoir des piles infinies, comme dans la programmation fonctionnelle (un paradigme de programmation qui ne fait qu'effleurer sa surface).

40
Tom

L'augmentation de la taille de la pile ne servira que de bandage temporaire. Comme d'autres l'ont souligné, ce que vous voulez vraiment, c'est éliminer les appels en queue, et Java ne l'a pas pour diverses raisons. Cependant, vous pouvez tricher si vous voulez.

La pilule rouge en main? OK, de cette façon s'il vous plaît.

Il y a des façons de remplacer pile par tas. Par exemple, au lieu de faire un appel récursif au sein d'une fonction, faites-lui renvoyer une structure de données lazy qui effectue l'appel lorsqu'il est évalué. Vous pouvez ensuite dérouler la "pile" avec la construction for for de Java. Je vais démontrer avec un exemple. Considérez ce code Haskell:

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

Notez que cette fonction n'évalue jamais la fin de la liste. Donc, la fonction n'a pas réellement besoin de faire un appel récursif. En Haskell, il renvoie en fait un thunk _ pour la queue, qui est appelée si nécessaire. Nous pouvons faire la même chose en Java (cela utilise des classes de Functional 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);}});}

Notez que Stream<A> consiste en une valeur de type A et une valeur de type P1, qui ressemble à un thunk qui renvoie le reste du flux lorsque _1 () est appelé. Bien que cela ressemble certainement à de la récursivité, l'appel récursif à mapper n'est pas effectué, mais fait désormais partie de la structure de données Stream.

Cela peut ensuite être décomposé avec un for-construct régulier.

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

Voici un autre exemple, puisque vous parliez du projet Euler. Ce programme utilise des fonctions mutuellement récursives et ne détruit pas la pile, même pour des millions d'appels:

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())));}}

Une autre chose que vous pouvez faire pour échanger pile contre tas consiste à utiliser plusieurs threads. L'idée est qu'au lieu de passer un appel récursif, vous créez un thunk qui passe l'appel, transmettez ce thunk à un nouveau thread et laissez le thread en cours quitter la fonction. C'est l'idée derrière des choses comme Python sans pile.

Voici un exemple de cela en Java. Excuses qu'il est un peu opaque de regarder sans les clauses 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 est soutenu par un pool de threads et la fonction promise remet un thunk au pool de threads, renvoyant une Promise, qui ressemble beaucoup à Java.util.concurrent.Future, mais en mieux. Vois ici. Le fait est que la méthode ci-dessus plie une structure de données récursive droite vers la droite dans O(1) pile, qui nécessite généralement une élimination de l'appel final. Nous avons donc effectivement atteint le TCE, en échange d’une certaine complexité. Vous appelleriez cette fonction comme suit:

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

Notez que cette dernière technique fonctionne parfaitement pour la récursion non linéaire. C'est-à-dire qu'il fonctionnera en pile constante, même dans les algorithmes sans appel de queue.

Une autre chose que vous pouvez faire est d’employer une technique appelée trampoline. Un trampoline est un calcul, réifié en tant que structure de données, qui peut être franchi. La bibliothèque fonctionnelle Java inclut un type de données Trampoline que j'ai écrit, qui vous permet de transformer n'importe quel appel de fonction en un appel final. A titre d'exemple voici une foldRightC trampoline qui se plie vers la droite en pile constante:

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()));}});}

C'est le même principe que lorsque vous utilisez plusieurs threads, sauf qu'au lieu d'invoquer chaque étape dans son propre thread, nous construisons chaque étape sur le tas, comme si vous utilisiez une variable Stream, puis exécutez toutes les étapes dans une boucle unique avec Trampoline.run.

86
Apocalisp

Il appartient à la machine virtuelle d'utiliser ou non la récursion de la queue. Je ne sais pas si c'est le cas, mais vous ne devriez pas vous en fier. En particulier, modifier la taille de la pile très serait rarement la bonne chose à faire, sauf si vous aviez une limite stricte quant au nombre de niveaux de récursivité que vous utiliseriez réellement, et si vous saviez exactement combien d'espace de pile chaque occuperait . Très fragile.

Fondamentalement, vous ne devriez pas utiliser une récursion sans limite dans un langage qui ne soit pas construit pour elle. Vous devrez utiliser l'itération à la place, j'ai bien peur. Et oui, cela peut parfois être une légère douleur :(

23
Jon Skeet

Si vous devez demander, vous faites probablement quelque chose de mal .

Maintenant, bien que vous puissiez probablement trouver un moyen d’augmenter la pile par défaut en Java, permettez-moi d’ajouter mes 2 centimes en ce sens que vous devez vraiment trouver un autre moyen de faire ce que vous voulez, au lieu de vous appuyer sur une pile plus grande.

Comme la spécification Java ne rend pas obligatoire pour les machines virtuelles Java d’implémenter des techniques d’optimisation de la récursion finale, le seul moyen de contourner le problème est de réduire la pression sur la pile, en réduisant le nombre de variables/paramètres locaux à conserver suivre ou idéalement en réduisant simplement le niveau de récursion ou en réécrivant sans récursion du tout.

La plupart des langages fonctionnels prennent en charge la récursion de la queue. Cependant, la plupart des compilateurs Java ne le supportent pas. Au lieu de cela, il effectue un autre appel de fonction. Cela signifie qu'il y aura toujours une limite supérieure sur le nombre d'appels récursifs que vous pouvez faire (car vous manquerez éventuellement d'espace de pile).

Avec la récursion de la queue, vous réutilisez le cadre de pile de la fonction qui est récursive, vous n'avez donc pas les mêmes contraintes sur la pile.

8
Sean

Vous pouvez définir ceci sur la ligne de commande:

Classe Java -Xss8M

7
stili

Clojure, qui fonctionne sur la machine virtuelle Java, aimerait beaucoup implémenter l'optimisation des appels en aval, mais ne peut pas le faire en raison d'une restriction dans le bytecode de la JVM (je ne connais pas les détails). En conséquence, il ne peut s’aider que grâce à un formulaire spécial "récurrent", qui implémente quelques fonctionnalités de base que vous pouvez attendre d’une récursion appropriée.

Quoi qu'il en soit, cela signifie que la machine virtuelle Java actuellement ne peut pas prendre en charge l'optimisation de l'appel final. Je suggère fortement de ne pas utiliser la récursivité comme construction de boucle générale sur la machine virtuelle Java. Mon point de vue personnel est que Java n’est pas un langage de niveau suffisamment élevé.

6
Svante
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())));
}
1
test

J'ai rencontré le même problème et j'ai fini par réécrire la récursion dans une boucle for et c'est tout.

0
Renaud