Comme JohannesD a suggéré dans un commentaire il n'est pas possible de compter de 0 à 1. Integer.MAX_VALUE
(et, après le débordement, de -Integer.MAX_VALUE
à 0 de nouveau) si rapidement.
Afin de vérifier l'hypothèse selon laquelle le JIT effectue ici une optimisation magique, j'ai créé un programme légèrement modifié, en introduisant certaines méthodes permettant d'identifier plus facilement certaines parties du code :
class IntOverflowTest
{
public static void main(String[] args) {
runLoop();
}
public static void runLoop()
{
int i = 1;
int k = 0;
while (true) {
if(++i==0) doPrint(++k);
}
}
public static void doPrint(int k)
{
System.out.println("loop: " + k);
}
}
Le bytecode émis et affiché avec javap -c IntOverflowTest
n'apporte aucune surprise :
class IntOverflowTest {
IntOverflowTest();
Code:
0: aload_0
1: invokespecial #1
4: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2
3: return
public static void runLoop();
Code:
0: iconst_1
1: istore_0
2: iconst_0
3: istore_1
4: iinc 0, 1
7: iload_0
8: ifne 4
11: iinc 1, 1
14: iload_1
15: invokestatic #3
18: goto 4
public static void doPrint(int);
Code:
0: getstatic #4
3: new #5
6: dup
7: invokespecial #6
10: ldc #7
12: invokevirtual #8
15: iload_0
16: invokevirtual #9
19: invokevirtual #10
22: invokevirtual #11
25: return
}
Il incrémente clairement les deux variables locales ( runLoop
les décalages 4 et 11).
Cependant, lorsque l'on exécute le code avec -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+PrintAssembly
dans un désassembleur Hotspot, le code machine finit par être le suivant :
Decoding compiled method 0x00000000025c2c50:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x000000001bb40408} 'runLoop' '()V' in 'IntOverflowTest'
# [sp+0x20] (sp of caller)
0x00000000025c2da0: mov %eax,-0x6000(%rsp)
0x00000000025c2da7: push %rbp
0x00000000025c2da8: sub $0x10,%rsp ;*synchronization entry
; - IntOverflowTest::runLoop@-1 (line 10)
0x00000000025c2dac: mov $0x1,%ebp ;*iinc
; - IntOverflowTest::runLoop@11 (line 13)
0x00000000025c2db1: mov %ebp,%edx
0x00000000025c2db3: callq 0x00000000024f6360 ; OopMap{off=24}
;*invokestatic doPrint
; - IntOverflowTest::runLoop@15 (line 13)
; {static_call}
0x00000000025c2db8: inc %ebp ;*iinc
; - IntOverflowTest::runLoop@11 (line 13)
0x00000000025c2dba: jmp 0x00000000025c2db1 ;*invokestatic doPrint
; - IntOverflowTest::runLoop@15 (line 13)
0x00000000025c2dbc: mov %rax,%rdx
0x00000000025c2dbf: add $0x10,%rsp
0x00000000025c2dc3: pop %rbp
0x00000000025c2dc4: jmpq 0x00000000025b0d20 ; {runtime_call}
0x00000000025c2dc9: hlt
On peut clairement voir qu'il n'incrémente pas la variable externe i
plus. Il appelle seulement le doPrint
incrémente une seule variable ( k
dans le code), puis et revient immédiatement au point précédant la doPrint
appeler.
Ainsi, le JIT semble effectivement détecter qu'il n'y a pas de réelle "condition" impliquée dans l'impression de la sortie, et que le code est équivalent à une boucle infinie qui ne fait qu'imprimer et incrémenter une seule variable.
Cela me semble être une optimisation assez sophistiquée. Je m'attendrais à ce qu'il soit loin d'être trivial de détecter un cas comme celui-ci. Mais visiblement, ils ont réussi à le faire...