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.
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);
}
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.StrangeBehaviour::functionB
Cela semble éliminer le retard avec la fonction plus courte.
avec les options vm, vous pouvez afficher inline -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
@ 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
@ 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
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 :
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
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.