web-dev-qa-db-fra.com

Comment augmenter la taille de la pile Java?

J'ai posé cette question pour savoir comment augmenter la taille de la pile d'appels d'exécution dans la machine virtuelle Java. J'ai une réponse à cela, et j'ai aussi beaucoup de réponses utiles et de commentaires pertinents sur la façon dont Java gère la situation dans laquelle une pile d'exécution importante est nécessaire. J'ai étendu ma question. avec le résumé des réponses.

À l'origine, je voulais augmenter la taille de la pile de la machine virtuelle Java afin que les programmes fonctionnent de manière similaire sans StackOverflowError.

public class TT {
  public static long fact(int n) {
    return n < 2 ? 1 : n * fact(n - 1);
  }
  public static void main(String[] args) {
    System.out.println(fact(1 << 15));
  }
}

Le paramètre de configuration correspondant est l'indicateur de ligne de commande Java -Xss... Avec une valeur suffisamment grande. Pour le programme TT ci-dessus, cela fonctionne comme ceci avec la machine virtuelle Java d'OpenJDK:

$ javac TT.Java
$ Java -Xss4m TT

Une des réponses a également indiqué que les indicateurs -X... Dépendaient de la mise en œuvre. J'utilisais

Java version "1.6.0_18"
OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1~8.04.3)
OpenJDK 64-Bit Server VM (build 16.0-b13, mixed mode)

Il est également possible de spécifier une pile volumineuse pour un seul thread (voir dans une des réponses comment). Ceci est recommandé sur Java -Xss... Pour éviter de gaspiller de la mémoire pour des threads qui n'en ont pas besoin.

J'étais curieux de la taille de la pile dont le programme ci-dessus a exactement besoin, je l'ai donc exécutée n a augmenté:

  • -Xss4m peut être suffisant pour fact(1 << 15)
  • -Xss5m peut être suffisant pour fact(1 << 17)
  • -Xss7m peut être suffisant pour fact(1 << 18)
  • -Xss9m peut être suffisant pour fact(1 << 19)
  • -Xss18m peut être suffisant pour fact(1 << 20)
  • -Xss35m peut être suffisant pour fact(1 << 21)
  • -Xss68m peut être suffisant pour fact(1 << 22)
  • -Xss129m peut être suffisant pour fact(1 << 23)
  • -Xss258m peut être suffisant pour fact(1 << 24)
  • -Xss515m peut être suffisant pour fact(1 << 25)

D'après les chiffres ci-dessus, il semble que Java utilise environ 16 octets par cadre de pile pour la fonction ci-dessus, ce qui est raisonnable.

L'énumération ci-dessus contient peut être suffisant au lieu de suffisant, car l'exigence de pile n'est pas déterministe: l'exécuter plusieurs fois avec le même fichier source et le même -Xss... réussit parfois et donne parfois un StackOverflowError. Par exemple. pour 1 << 20, -Xss18m était suffisant dans 7 courses sur 10, et -Xss19m n'était pas toujours suffisant non plus, mais -Xss20m était suffisant (au total, 100 séries) de 100). Est-ce que le ramassage des ordures, le démarrage de l’ECI ou autre chose sont à l’origine de ce comportement non déterministe?

La trace de pile imprimée à un StackOverflowError (et éventuellement à d'autres exceptions également) montre uniquement les 1024 éléments les plus récents de la pile d'exécution. Une réponse ci-dessous montre comment compter la profondeur exacte atteinte (qui peut être beaucoup plus grande que 1024).

De nombreuses personnes qui ont répondu ont fait remarquer que le fait d’envisager des implémentations alternatives, moins gourmandes en pile, du même algorithme est une bonne pratique de codage. En général, il est possible de convertir un ensemble de fonctions récursives en fonctions itératives (en utilisant un objet, par exemple Stack, qui est rempli sur le tas au lieu de sur la pile d'exécution). Pour cette fonction fact particulière, il est assez facile de la convertir. Ma version itérative ressemblerait à ceci:

public class TTIterative {
  public static long fact(int n) {
    if (n < 2) return 1;
    if (n > 65) return 0;  // Enough powers of 2 in the product to make it (long)0.
    long f = 2;
    for (int i = 3; i <= n; ++i) {
      f *= i;
    }
    return f;
  }
  public static void main(String[] args) {
    System.out.println(fact(1 << 15));
  }
}

Pour info, comme le montre la solution itérative ci-dessus, la fonction fact ne peut pas calculer la factorielle exacte des nombres supérieurs à 65 (en fait, même supérieurs à 20), car Java intégré Le type long déborderait. Refactoriser fact afin de renvoyer un BigInteger au lieu de long donnerait également des résultats exacts pour les entrées volumineuses.

111
pts

Hmm ... cela fonctionne pour moi et avec moins de 999 Mo de pile:

> Java -Xss4m Test
0

(Windows JDK 7, VM cliente build 17.0-b05 et Linux JDK 6 - informations de version identiques à celles que vous avez publiées)

70
Jon Skeet

Je suppose que vous avez calculé la "profondeur de 1024" par les lignes récurrentes dans la trace de la pile?

De toute évidence, la longueur du tableau de trace de pile dans Throwable semble être limitée à 1024. Essayez le programme suivant:

public class Test {

    public static void main(String[] args) {

        try {
            System.out.println(fact(1 << 15));
        }
        catch (StackOverflowError e) {
            System.err.println("true recursion level was " + level);
            System.err.println("reported recursion level was " +
                               e.getStackTrace().length);
        }
    }

    private static int level = 0;
    public static long fact(int n) {
        level++;
        return n < 2 ? n : n * fact(n - 1);
    }
}
11
Jay

Si vous voulez jouer avec la taille de la pile de threads, vous devriez regarder l'option -Xss sur la JVM Hotspot. Cela peut être différent sur les machines virtuelles non Hotspot puisque les paramètres -X de la JVM sont spécifiques à la distribution, IIRC.

Sur Hotspot, cela ressemble à Java -Xss16M si vous voulez faire la taille 16 Mo.

Tapez Java -X -help si vous voulez voir tous les paramètres JVM spécifiques à la distribution, vous pouvez les indiquer. Je ne sais pas si cela fonctionne de la même manière que sur d’autres JVM, mais cela affiche tous les paramètres spécifiques à Hotspot.

Pour ce qui en vaut la peine, je vous recommande de limiter votre utilisation de méthodes récursives en Java. Ce n’est pas très bon pour les optimiser - par exemple, la JVM ne prend pas en charge la récursion finale (voir La JVM empêche-t-elle les optimisations d’appel final? ). Essayez de refactoriser votre code factoriel ci-dessus pour utiliser une boucle while au lieu d'appels de méthode récursifs.

9
whaley

Le seul moyen de contrôler la taille de la pile dans le processus est de lancer un nouveau Thread. Mais vous pouvez également contrôler en créant un sous-processus auto-appelant Java avec le processus -Xss paramètre.

public class TT {
    private static int level = 0;

    public static long fact(int n) {
        level++;
        return n < 2 ? n : n * fact(n - 1);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(null, null, "TT", 1000000) {
            @Override
            public void run() {
                try {
                    level = 0;
                    System.out.println(fact(1 << 15));
                } catch (StackOverflowError e) {
                    System.err.println("true recursion level was " + level);
                    System.err.println("reported recursion level was "
                            + e.getStackTrace().length);
                }
            }

        };
        t.start();
        t.join();
        try {
            level = 0;
            System.out.println(fact(1 << 15));
        } catch (StackOverflowError e) {
            System.err.println("true recursion level was " + level);
            System.err.println("reported recursion level was "
                    + e.getStackTrace().length);
        }
    }

}
8
Dennis C

Ajouter cette option

--driver-Java-options -Xss512m

à votre commande spark-submit va résoudre ce problème.

3
Guibin Zhang

Il est difficile de donner une solution sensée, car vous souhaitez éviter toute approche sensée. La refactorisation d’une ligne de code est la solution envisageable.

Remarque: L'utilisation de -Xss définit la taille de la pile de chaque thread et est une très mauvaise idée.

Une autre approche est la manipulation de code d'octet pour modifier le code comme suit;

public static long fact(int n) { 
    return n < 2 ? n : n > 127 ? 0 : n * fact(n - 1); 
}

étant donné que chaque réponse pour n> 127 est 0. Ceci évite de changer le code source.

1
Peter Lawrey

J'ai fait Anagram excersize , ce qui revient à problème de changement de compte mais avec 50 000 dénominations (pièces). Je suis pas sûr que cela puisse se faire de manière itérative , je m'en fiche. Je sais juste que l’option -xss n’a aucun effet - j’échoue toujours après 1024 cadres de pile (peut-être scala effectue une mauvaise tâche en remettant à to Java ou printStackTrace (je ne sais pas). C’est une mauvaise option, comme expliqué de toute façon. Vous ne voulez pas que tous les threads de l’application soient monstrueux. Cependant, j’ai fait quelques expériences avec le nouveau Thread (taille de la pile).

  def measureStackDepth(ss: Long): Long = {
    var depth: Long = 0
      val thread: Thread = new Thread(null, new Runnable() {
        override def run() {
          try {
          def sum(n: Long): Long = {depth += 1; if (n== 0) 0 else sum(n-1) + 1}
          println("fact = " + sum(ss * 10))
          } catch {
            case e: StackOverflowError => // eat the exception, that is expected
          }
        }
      }, "deep stack for money exchange", ss)
      thread.start()
      thread.join()
    depth
  }                                               //> measureStackDepth: (ss: Long)Long


  for (ss <- (0 to 10)) println("ss = 10^" +  ss + " allows stack of size " -> measureStackDepth((scala.math.pow (10, ss)).toLong) )
                                                  //> fact = 10
                                                  //| (ss = 10^0 allows stack of size ,11)
                                                  //| fact = 100
                                                  //| (ss = 10^1 allows stack of size ,101)
                                                  //| fact = 1000
                                                  //| (ss = 10^2 allows stack of size ,1001)
                                                  //| fact = 10000
                                                  //| (ss = 10^3 allows stack of size ,10001)
                                                  //| (ss = 10^4 allows stack of size ,1336)
                                                  //| (ss = 10^5 allows stack of size ,5456)
                                                  //| (ss = 10^6 allows stack of size ,62736)
                                                  //| (ss = 10^7 allows stack of size ,623876)
                                                  //| (ss = 10^8 allows stack of size ,6247732)
                                                  //| (ss = 10^9 allows stack of size ,62498160)

Vous voyez que la pile peut croître de manière exponentielle avec exponentiellement plus de pile allouée au thread.

0
Val

Bizarre! Vous dites que vous voulez générer un récursion de 1 << 15 profondeur ??? !!!!

Je suggère de NE PAS l'essayer. La taille de la pile sera 2^15 * sizeof(stack-frame). Je ne sais pas ce qu'est la taille d'une pile, mais 2 ^ 15 correspond à 32,768. Assez bien ... Eh bien, si elle s’arrête à 1024 (2 ^ 10), vous devrez le faire 2 ^ 5 fois plus grand, il est 32 fois plus grand que dans votre réglage actuel.

0
helios

D'autres affiches ont indiqué comment augmenter la mémoire et que vous pouviez mémoriser des appels. Je suggérerais que pour de nombreuses applications, vous pouvez utiliser la formule de Stirling pour approximer les gros n! très rapidement avec presque pas d'empreinte mémoire.

Jetez un coup d'œil à ce message, qui présente une analyse de la fonction et du code:

http://threebrothers.org/brendan/blog/stirlings-approximation-formula-clojure/

0
abscondment