Comme déjà souligné, JIT (just-in-time) peut optimiser une boucle vide afin de supprimer les itérations inutiles. Mais comment ?
En fait, il existe deux compilateurs JIT : C1 & C2 . D'abord, le code est compilé avec le C1. C1 collecte les statistiques et aide la JVM à découvrir que dans 100% des cas notre boucle vide ne change rien et est inutile. Dans cette situation, C2 entre en scène. Lorsque le code est appelé très souvent, il peut être optimisé et compilé avec le C2 en utilisant les statistiques collectées.
À titre d'exemple, je vais tester l'extrait de code suivant (mon JDK est réglé sur slowdebug build 9-interne ) :
public class Demo {
private static void run() {
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
}
System.out.println("Done!");
}
}
Avec les options de ligne de commande suivantes :
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Demo.run
Et il y a différentes versions de mon exécuter compilé avec les C1 et C2 de manière appropriée. Pour moi, la variante finale (C2) ressemble à quelque chose comme ceci :
...
; B1: # B3 B2 <- BLOCK HEAD IS JUNK Freq: 1
0x00000000125461b0: mov dword ptr [rsp+0ffffffffffff7000h], eax
0x00000000125461b7: push rbp
0x00000000125461b8: sub rsp, 40h
0x00000000125461bc: mov ebp, dword ptr [rdx]
0x00000000125461be: mov rcx, rdx
0x00000000125461c1: mov r10, 57fbc220h
0x00000000125461cb: call indirect r10 ; *iload_1
0x00000000125461ce: cmp ebp, 7fffffffh ; 7fffffff => 2147483647
0x00000000125461d4: jnl 125461dbh ; jump if not less
; B2: # B3 <- B1 Freq: 0.999999
0x00000000125461d6: mov ebp, 7fffffffh ; *if_icmpge
; B3: # N44 <- B1 B2 Freq: 1
0x00000000125461db: mov edx, 0ffffff5dh
0x0000000012837d60: nop
0x0000000012837d61: nop
0x0000000012837d62: nop
0x0000000012837d63: call 0ae86fa0h
...
C'est un peu désordonné, mais si vous regardez de près, vous remarquerez qu'il n'y a pas de longue boucle ici. Il y a 3 blocs : B1, B2 et B3 et les étapes d'exécution peuvent être B1 -> B2 -> B3
ou B1 -> B3
. Où Freq: 1
- fréquence estimée normalisée de l'exécution d'un bloc.
69 votes
Eh bien oui. Comme le corps de la boucle n'a pas d'effets secondaires, le compilateur l'élimine allègrement. Examinez le byte-code avec
javap -v
à voir.0 votes
@ElliottFrisch pouvez-vous nous en dire un peu plus ?
36 votes
Vous ne verrez pas ça dans le byte-code.
javac
n'effectue que très peu d'optimisation réelle et en laisse la majeure partie au compilateur JIT.0 votes
Dans Eclipse, si vous exécutez sans points d'arrêt, la boucle est optimisée. Si vous placez un point d'arrêt n'importe où, il exécute la boucle.
0 votes
Les processeurs Intel sont également optimisés de telle sorte que les instructions répétées sont mises en cache et ne nécessitent donc pas de recalcul de la logique. Seule la partie des données qui est calculée n'est pas entièrement mise en cache (mais devinée avec précision). Cela pourrait aussi avoir un rapport avec l'efficacité extrême.
4 votes
Je me demande si c'est le résultat d'une optimisation de la JVM ? - Qu'en pensez-vous ? Qu'est-ce que cela pourrait être d'autre si ce n'est une optimisation de la JVM ?
7 votes
La réponse à cette question est essentiellement contenue dans stackoverflow.com/a/25323548/3182664 . Il contient également l'assemblage résultant (code machine) que le JIT génère pour de tels cas, montrant que la boucle est complètement optimisée par le JIT. . (La question à stackoverflow.com/q/25326377/3182664 montre que cela peut prendre un peu plus de temps si la boucle ne fait pas 4 milliards d'opérations, mais 4 milliards de moins un ;-) ). Je considérerais presque que cette question fait double emploi avec l'autre - des objections ?
1 votes
@ElliottFrisch Sérieusement, avec près de 134 mille points de réputation vous auriez dû voir le message imprimé en écrivant un commentaire : Évitez de répondre aux questions dans les commentaires. C'est là pour une raison.
0 votes
Voici quelques informations sur les optimisations des compilateurs et sur la manière dont elles utilisent le processeur réel plutôt que le x86 virtuel sur lequel nous programmons. blogs.msdn.microsoft.com/oldnewthing/20041216-00/?p=36973 et blogs.msdn.microsoft.com/oldnewthing/20140627-00/?p=633
7 votes
Vous supposez que le processeur effectuera une itération par Hz. C'est une hypothèse lourde de conséquences. Les processeurs d'aujourd'hui effectuent toutes sortes d'optimisations, comme @Rahul l'a mentionné, et à moins d'en savoir beaucoup plus sur le fonctionnement du Core i7, vous ne pouvez pas supposer cela.
0 votes
En plus de ce que dit Tsahi, pour chaque boucle le processeur doit, au moins : augmenter le compteur, vérifier, sauter si. Il n'y a aucune chance que cela se produise en un seul cycle.
0 votes
Java est en fait aussi lent que de prendre 2 ms pour terminer une expression de boucle sans effet optimisée.
0 votes
@hkBattousai C'est un bon point, mais je suis sceptique - la résolution de la minuterie dépend de la plateforme, et les mesures elles-mêmes sont coûteuses, notamment nanotime. Plus d'informations trouvé via une autre question sur le SO . Pour être clair, le nanotime est le bon outil pour ce travail ("temps écoulé augmentant de façon monotone"), il suffit de garder l'"effet observateur" à l'esprit.
1 votes
@hkBattousai : c'est une exécution unique au sein de la
main
méthode. Elle commence donc sans être optimisée, interprétée, avant qu'à un moment donné, l'optimiseur n'intervienne. Un tel scénario dépend également de la présence d'un remplacement sur la pile, pour que la version optimisée soit effective. C'est pourquoi les vrais microbenchmarks fonctionnent de manière totalement différente.