web-dev-qa-db-fra.com

Java s'exécute plus lentement lorsque le code qui n'est jamais exécuté est mis en commentaire

J'ai observé un comportement étrange dans l'un de mes programmes Java. J'ai essayé de supprimer le code autant que possible tout en étant capable de reproduire le comportement. Code complet ci-dessous.

public class StrangeBehaviour {

    static boolean recursionFlag = true;

    public static void main(String[] args) {
        long startTime = System.nanoTime();
        for (int i = 0; i < 10000; i ++) {
            functionA(6, 0);
        }
        long endTime = System.nanoTime();
        System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
    }

    static boolean functionA(int recursionDepth, int recursionSwitch) {
        if (recursionDepth == 0) { return true; }
        return functionB(recursionDepth, recursionSwitch);
    }

    static boolean functionB(int recursionDepth, int recursionSwitch) {
        for (int i = 0; i < 16; i++) {
            if (StrangeBehaviour.recursionFlag) {
                if (recursionSwitch == 0) {
                    if (functionA(recursionDepth - 1, 1 - recursionSwitch)) return true;
                } else {
                    if (!functionA(recursionDepth - 1, 1 - recursionSwitch)) return false;
                }
            } else {
                // This block is never entered into.
                // Yet commenting out one of the lines below makes the program run slower!
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
                System.out.println("...");
            }
        }
        return false;
    }
}

J'ai deux fonctions, functionA() et functionB() qui s'appellent récursivement. Les deux fonctions prennent un paramètre recursionDepth qui contrôle la fin de la récursivité. functionA() appelle functionB() au maximum une fois avec recursionDepth inchangé. functionB() appelle functionA() 16 fois avec recursionDepth - 1. La récursivité se termine lorsque functionA() est appelée avec un recursionDepth de 0.

functionB() a un bloc de code avec un certain nombre d'appels System.out.println(). Ce bloc n'est jamais entré, car l'entrée est contrôlée par une variable boolean recursionFlag Qui est définie sur true et qui n'est jamais modifiée pendant l'exécution du programme. Toutefois, la mise en commentaire d'un seul des appels println() ralentit le programme. Sur ma machine, le temps d'exécution est <0,2 s avec tous les appels println() présents et> 2 s lorsque l'un des appels est commenté.

Quelle pourrait être la cause de ce comportement? Ma seule supposition est qu'il existe une optimisation naïve du compilateur qui est déclenchée par un paramètre lié à la longueur du bloc de code (ou au nombre d'appels de fonction, etc.). Toute information supplémentaire à ce sujet sera très appréciée!

Edit: j'utilise JDK 1.8.

57
J3D1

La réponse complète est une combinaison de k5_ et des réponses de Tony.

Le code que l'OP a publié omet une boucle de préchauffage pour déclencher la compilation HotSpot avant de faire le test; Par conséquent, l'accélération 10 fois (sur mon ordinateur) lorsque les instructions d'impression sont incluses, combine à la fois le temps passé dans HotSpot pour compiler le bytecode en instructions CPU, ainsi que l'exécution réelle des instructions CPU.

Si j'ajoute une boucle d'échauffement distincte avant la boucle de synchronisation, il n'y a qu'une accélération de 2,5 fois avec l'instruction print.

Cela indique que la compilation HotSpot/JIT prend plus de temps lorsque la méthode est en ligne (comme l'explique Tony) ainsi que l'exécution du code prend plus de temps, probablement en raison des performances de cache ou de prédiction de branche/pipelining inférieures, comme l'a montré k5_.

public static void main(String[] args) {
    // Added the following warmup loop before the timing loop
    for (int i = 0; i < 50000; i++) {
        functionA(6, 0);
    }

    long startTime = System.nanoTime();
    for (int i = 0; i < 50000; i++) {
        functionA(6, 0);
    }
    long endTime = System.nanoTime();
    System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
}
21
Erwin Bolwidt

Le code commenté affecte la façon dont l'inlining est géré. Si la fonction B devient plus longue/plus grande (plus d'instructions de bytecode), elle ne sera pas insérée dans la fonctionA.

@ J3D1 a donc pu utiliser VMOptions pour désactiver manuellement l'inline pour la fonction B (): -XX:CompileCommand=dontinline,com.jd.benchmarking.StrangeBeh‌​aviour::functionB Cela semble éliminer le retard avec la fonction plus courte.

avec les options vm, vous pouvez afficher inline -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

La version plus grande, ne fonctionne pas en ligneB

@ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large
@ 21   StrangeBehaviour::functionA (12 bytes)
  @ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large
@ 35   StrangeBehaviour::functionA (12 bytes)
  @ 8   StrangeBehaviour::functionB (326 bytes)   callee is too large

La version plus courte essaiera d'intégrer la fonction B, provoquant quelques tentatives supplémentaires.

@ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
 @ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
   @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
     @ 35   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
 @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
   @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
     @ 21   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
     @ 35   StrangeBehaviour::functionA (12 bytes)   recursive inlining is too deep
@ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
 @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
   @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
     @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep
@ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
 @ 8   StrangeBehaviour::functionB (318 bytes)   inline (hot)
   @ 21   StrangeBehaviour::functionA (12 bytes)   inline (hot)
    @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep
   @ 35   StrangeBehaviour::functionA (12 bytes)   inline (hot)
     @ 8   StrangeBehaviour::functionB (318 bytes)   recursive inlining is too deep

Surtout deviner, mais le bytecode plus grand/en ligne causera des problèmes avec la prédiction de branche et la mise en cache

41
k5_

Je suis avec @ k5_, il semble qu'il existe un seuil pour déterminer s'il faut incorporer une fonction. Et si le compilateur JIT décide de l'intégrer, cela entraînera beaucoup de travail et de temps pour le faire comme -XX:+PrintCompilation spectacles:

  task-id
    158   32       3       so_test.StrangeBehaviour::functionB (326 bytes)   made not entrant
    159   35       3       Java.lang.String::<init> (82 bytes)
    160   36  s    1       Java.util.Vector::size (5 bytes)
    1878   37 %     3       so_test.StrangeBehaviour::main @ 6 (65 bytes)
    1898   38       3       so_test.StrangeBehaviour::main (65 bytes)
    2665   39       3       Java.util.regex.Pattern::has (15 bytes)
    2667   40       3       Sun.misc.FDBigInteger::mult (64 bytes)
    2668   41       3       Sun.misc.FDBigInteger::<init> (30 bytes)
    2668   42       3       Sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    2.51 seconds elapsed.

La partie supérieure est info sans commentaire, ce qui suit est avec un commentaire qui réduit la taille de la méthode de 326 octets à 318 octets. Et vous pouvez remarquer que l'ID de tâche dans la colonne 1 de la sortie , est tellement plus grand dans ce dernier, ce qui entraîne plus de temps.

  task-id
    126   35       4       so_test.StrangeBehaviour::functionA (12 bytes)
    130   33       3       so_test.StrangeBehaviour::functionA (12 bytes)   made not entrant
    131   36  s    1       Java.util.Vector::size (5 bytes)
    14078   37 %     3       so_test.StrangeBehaviour::main @ 6 (65 bytes)
    14296   38       3       so_test.StrangeBehaviour::main (65 bytes)
    14296   39 %     4       so_test.StrangeBehaviour::functionB @ 2 (318 bytes)
    14300   40       4       so_test.StrangeBehaviour::functionB (318 bytes)
    14304   34       3       so_test.StrangeBehaviour::functionB (318 bytes)   made not entrant
    14628   41       3       Java.util.regex.Pattern::has (15 bytes)
    14631   42       3       Sun.misc.FDBigInteger::mult (64 bytes)
    14632   43       3       Sun.misc.FDBigInteger::<init> (30 bytes)
    14632   44       3       Sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    14.50 seconds elapsed.

Et si vous changez le code en suivant (ajoutez deux lignes et commnet une ligne d'impression), vous pouvez voir que la taille du code change en 326 octets et s'exécute plus rapidement maintenant:

        if (StrangeBehaviour.recursionFlag) {
            int a = 1;
            int b = 1;
            if (recursionSwitch == 0) {
                if (functionA(recursionDepth - 1, 1 - recursionSwitch)) return true;
            } else {
                if (!functionA(recursionDepth - 1, 1 - recursionSwitch)) return false;
            }
        } else {
            // This block is never entered into.
            // Yet commenting out one of the lines below makes the program run slower!
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
          //System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
            System.out.println("...");
        }

Nouvelle heure et informations sur le compilateur JIT:

    140   34       3       so_test.StrangeBehaviour::functionB (326 bytes)   made not entrant
    145   36       3       Java.lang.String::<init> (82 bytes)
    148   37  s    1       Java.util.Vector::size (5 bytes)
    162   38       4       so_test.StrangeBehaviour::functionA (12 bytes)
    163   33       3       so_test.StrangeBehaviour::functionA (12 bytes)   made not entrant
    1916   39 %     3       so_test.StrangeBehaviour::main @ 6 (65 bytes)
    1936   40       3       so_test.StrangeBehaviour::main (65 bytes)
    2686   41       3       Java.util.regex.Pattern::has (15 bytes)
    2689   42       3       Sun.misc.FDBigInteger::mult (64 bytes)
    2690   43       3       Sun.misc.FDBigInteger::<init> (30 bytes)
    2690   44       3       Sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
    2.55 seconds elapsed.

En conclusion :

  • Lorsque la taille de la méthode dépasse certaines limites, JIT n'inline pas cette fonction;
  • Et si nous commentons une ligne, qui se réduit à une taille inférieure au seuil, JIT décide de l'intégrer;
  • L'intégration de cette fonction entraîne de nombreuses tâches JIT, ce qui ralentit le programme.

mise à jour :

Selon mon dernier essai , la réponse à cette question n'est pas si simple:

Comme le montre mon exemple de code, une optimisation en ligne normale

  • accélère le programme
  • et ne coûte pas beaucoup de travail de compilateur (dans mon test, cela coûte même moins de travail lorsque le processus en ligne se produit).

Mais dans ce problème, le code cause beaucoup de travail JIT et ralentit le programme qui semble être un bogue de JIT. Et on ne sait toujours pas pourquoi cela cause autant de travail de JIT.

18
Tony