J'utilise le code Java suivant sur un ordinateur portable doté d'un processeur Intel Core i7 cadencé à 2,7 GHz. J'avais l'intention de le laisser mesurer le temps qu'il fallait pour terminer une boucle avec 2 ^ 32 itérations, ce qui, selon moi, devait durer environ 1,48 seconde (4/2,7 = 1,48).
Mais en réalité, cela ne prend que 2 millisecondes, au lieu de 1,48 s. Je me demande si cela résulte d'une optimisation de la JVM en dessous?
public static void main(String[] args)
{
long start = System.nanoTime();
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++){
}
long finish = System.nanoTime();
long d = (finish - start) / 1000000;
System.out.println("Used " + d);
}
Il y a l'une des deux possibilités suivantes:
Le compilateur s'est rendu compte que la boucle était redondante et ne faisait rien, il l'a donc optimisée.
Le compilateur juste-à-temps (JIT) a réalisé que la boucle est redondante et ne fait rien et l'a optimisée.
Les compilateurs modernes sont très intelligents. ils peuvent voir quand le code est inutile. Essayez de mettre une boucle vide dans GodBolt et regardez la sortie, puis activez les optimisations -O2
, vous verrez que la sortie est quelque chose du genre
main():
xor eax, eax
ret
J'aimerais clarifier quelque chose. Java la plupart des optimisations sont effectuées par le JIT. Dans d'autres langages (comme C/C++), la plupart des optimisations sont effectuées par le premier compilateur.
Il semble que cela ait été optimisé par le compilateur JIT. Quand je l'éteins (-Djava.compiler=NONE
), le code est beaucoup plus lent:
$ javac MyClass.Java
$ Java MyClass
Used 4
$ Java -Djava.compiler=NONE MyClass
Used 40409
Je mets le code de l'OP à l'intérieur de class MyClass
.
Je vais simplement préciser ce qui est évident: il s’agit d’une optimisation de la machine virtuelle, la boucle sera tout simplement supprimée. Voici un petit test qui montre quelle énorme différence JIT
a été activée/activée uniquement pour C1 Compiler
et désactivée du tout.
Avertissement: n'écrivez pas des tests comme celui-ci - ceci est juste pour prouver que la "suppression" de la boucle se produit dans le C2 Compiler
:
@Benchmark
@Fork(1)
public void full() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
++result;
}
}
@Benchmark
@Fork(1)
public void minusOne() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
++result;
}
}
@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-XX:TieredStopAtLevel=1" })
public void withoutC2() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
++result;
}
}
@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-Xint" })
public void withoutAll() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
++result;
}
}
Les résultats montrent que, selon la partie de la variable JIT
activée, la méthode est plus rapide (tellement plus rapide qu’elle donne l’impression de ne rien faire - suppression de la boucle, ce qui semble se produire dans le C2 Compiler
- quel est le niveau maximum):
Benchmark Mode Cnt Score Error Units
Loop.full avgt 2 ≈ 10⁻⁷ ms/op
Loop.minusOne avgt 2 ≈ 10⁻⁶ ms/op
Loop.withoutAll avgt 2 51782.751 ms/op
Loop.withoutC2 avgt 2 1699.137 ms/op
Comme déjà indiqué, le compilateur JIT (juste à temps) peut optimiser une boucle vide afin de supprimer les itérations inutiles. Mais comment?
En fait, il existe deux compilateurs JIT: C1 & C2. Tout d'abord, le code est compilé avec le C1. C1 collecte les statistiques et aide la machine virtuelle à 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.
Par exemple, je testerai le prochain extrait de code (mon JDK est défini sur slowdebug build 9-internal ):
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 existe différentes versions de ma méthode run, compilées de manière appropriée avec C1 et C2. Pour moi, la variante finale (C2) ressemble à 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 brouillon, mais si vous y regardez de près, vous remarquerez peut-être qu'il n'y a pas de longue boucle en cours 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.
Vous mesurez le temps nécessaire pour détecter la boucle ne fait rien, compilez le code dans un thread d'arrière-plan et éliminez le code.
for (int t = 0; t < 5; t++) {
long start = System.nanoTime();
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
}
long time = System.nanoTime() - start;
String s = String.format("%d: Took %.6f ms", t, time / 1e6);
Thread.sleep(50);
System.out.println(s);
Thread.sleep(50);
}
Si vous exécutez ceci avec -XX:+PrintCompilation
, vous pouvez voir que le code a été compilé en arrière-plan du compilateur de niveau 3 ou C1 et après quelques boucles au niveau 4 de C4.
129 34 % 3 A::main @ 15 (93 bytes)
130 35 3 A::main (93 bytes)
130 36 % 4 A::main @ 15 (93 bytes)
131 34 % 3 A::main @ -2 (93 bytes) made not entrant
131 36 % 4 A::main @ -2 (93 bytes) made not entrant
0: Took 2.510408 ms
268 75 % 3 A::main @ 15 (93 bytes)
271 76 % 4 A::main @ 15 (93 bytes)
274 75 % 3 A::main @ -2 (93 bytes) made not entrant
1: Took 5.629456 ms
2: Took 0.000000 ms
3: Took 0.000364 ms
4: Took 0.000365 ms
Si vous changez la boucle pour utiliser un long
, elle ne sera pas optimisée.
for (long i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
}
à la place vous obtenez
0: Took 1579.267321 ms
1: Took 1674.148662 ms
2: Took 1885.692166 ms
3: Took 1709.870567 ms
4: Took 1754.005112 ms