Apparemment, sur mon ordinateur portable Windows 8 avec HotSpot JDK 1.7.0_45 (avec toutes les options du compilateur/VM définies par défaut), la boucle ci-dessous
final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {
}
est au moins 2 ordres de grandeur plus rapide (~ 10 ms vs ~ 5000 ms) que:
final int n = Integer.MAX_VALUE;
int i = 0;
while (i++ < n) {
}
J'ai remarqué ce problème lors de l'écriture d'une boucle pour évaluer un autre problème de performances non pertinent. Et la différence entre ++i < n
et i++ < n
était assez énorme pour influencer de manière significative le résultat.
Si nous regardons le bytecode, le corps de la boucle de la version plus rapide est:
iinc
iload
ldc
if_icmplt
Et pour la version plus lente:
iload
iinc
ldc
if_icmplt
Donc pour ++i < n
, il incrémente d'abord la variable locale i
de 1, puis la pousse sur la pile d'opérandes pendant que i++ < n
effectue ces 2 étapes dans l'ordre inverse. Mais cela ne semble pas expliquer pourquoi le premier est beaucoup plus rapide. Y a-t-il une copie temporaire impliquée dans ce dernier cas? Ou est-ce quelque chose au-delà du bytecode (implémentation VM, matériel, etc.) qui devrait être responsable de la différence de performance?
J'ai lu d'autres discussions concernant ++i
et i++
(mais pas de manière exhaustive), mais n'a trouvé aucune réponse spécifique à Java et directement liée au cas où ++i
ou i++
participe à une comparaison de valeurs.
Comme d'autres l'ont souligné, le test présente de nombreux défauts.
Vous ne nous avez pas dit exactement comment vous avez fait ce test. Cependant, j'ai essayé d'implémenter un test "naïf" (sans infraction) comme ceci:
class PrePostIncrement
{
public static void main(String args[])
{
for (int j=0; j<3; j++)
{
for (int i=0; i<5; i++)
{
long before = System.nanoTime();
runPreIncrement();
long after = System.nanoTime();
System.out.println("pre : "+(after-before)/1e6);
}
for (int i=0; i<5; i++)
{
long before = System.nanoTime();
runPostIncrement();
long after = System.nanoTime();
System.out.println("post : "+(after-before)/1e6);
}
}
}
private static void runPreIncrement()
{
final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {}
}
private static void runPostIncrement()
{
final int n = Integer.MAX_VALUE;
int i = 0;
while (i++ < n) {}
}
}
Lorsque vous exécutez cela avec les paramètres par défaut, il semble y avoir une petite différence. Mais la faille réelle du benchmark devient évidente lorsque vous exécutez cela avec le -server
drapeau. Les résultats dans mon cas sont alors quelque chose comme
...
pre : 6.96E-4
pre : 6.96E-4
pre : 0.001044
pre : 3.48E-4
pre : 3.48E-4
post : 1279.734543
post : 1295.989086
post : 1284.654267
post : 1282.349093
post : 1275.204583
De toute évidence, la version pré-incrémentée a été complètement optimisée . La raison est assez simple: le résultat n'est pas utilisé. Peu importe que la boucle soit exécutée ou non, le JIT la supprime simplement.
Ceci est confirmé par un regard sur le démontage du hotspot: La version pré-incrémentée donne ce code:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x0000000055060500} 'runPreIncrement' '()V' in 'PrePostIncrement'
# [sp+0x20] (sp of caller)
0x000000000286fd80: sub $0x18,%rsp
0x000000000286fd87: mov %rbp,0x10(%rsp) ;*synchronization entry
; - PrePostIncrement::runPreIncrement@-1 (line 28)
0x000000000286fd8c: add $0x10,%rsp
0x000000000286fd90: pop %rbp
0x000000000286fd91: test %eax,-0x243fd97(%rip) # 0x0000000000430000
; {poll_return}
0x000000000286fd97: retq
0x000000000286fd98: hlt
0x000000000286fd99: hlt
0x000000000286fd9a: hlt
0x000000000286fd9b: hlt
0x000000000286fd9c: hlt
0x000000000286fd9d: hlt
0x000000000286fd9e: hlt
0x000000000286fd9f: hlt
La version post-incrémentation donne ce code:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x00000000550605b8} 'runPostIncrement' '()V' in 'PrePostIncrement'
# [sp+0x20] (sp of caller)
0x000000000286d0c0: sub $0x18,%rsp
0x000000000286d0c7: mov %rbp,0x10(%rsp) ;*synchronization entry
; - PrePostIncrement::runPostIncrement@-1 (line 35)
0x000000000286d0cc: mov $0x1,%r11d
0x000000000286d0d2: jmp 0x000000000286d0e3
0x000000000286d0d4: nopl 0x0(%rax,%rax,1)
0x000000000286d0dc: data32 data32 xchg %ax,%ax
0x000000000286d0e0: inc %r11d ; OopMap{off=35}
;*goto
; - PrePostIncrement::runPostIncrement@11 (line 36)
0x000000000286d0e3: test %eax,-0x243d0e9(%rip) # 0x0000000000430000
;*goto
; - PrePostIncrement::runPostIncrement@11 (line 36)
; {poll}
0x000000000286d0e9: cmp $0x7fffffff,%r11d
0x000000000286d0f0: jl 0x000000000286d0e0 ;*if_icmpge
; - PrePostIncrement::runPostIncrement@8 (line 36)
0x000000000286d0f2: add $0x10,%rsp
0x000000000286d0f6: pop %rbp
0x000000000286d0f7: test %eax,-0x243d0fd(%rip) # 0x0000000000430000
; {poll_return}
0x000000000286d0fd: retq
0x000000000286d0fe: hlt
0x000000000286d0ff: hlt
Il n'est pas tout à fait clair pour moi pourquoi il ne semble pas pas supprimer la version post-incrémentation. (En fait, je considère que poser cette question comme une question distincte). Mais au moins, cela explique pourquoi vous pourriez voir des différences avec un "ordre de grandeur" ...
EDIT: Fait intéressant, lors du changement de la limite supérieure de la boucle de Integer.MAX_VALUE
à Integer.MAX_VALUE-1
, puis les deux versions sont optimisées et nécessitent un temps "zéro". D'une manière ou d'une autre, cette limite (qui apparaît toujours sous la forme 0x7fffffff
dans l'assemblage) empêche l'optimisation. Vraisemblablement, cela a quelque chose à voir avec la comparaison étant mappée à une instruction (chantée!) cmp
, mais je ne peux pas donner une raison profonde au-delà de cela. Le JIT fonctionne de façon mystérieuse ...
La différence entre ++ i et i ++ est que ++ i incrémente efficacement la variable et "renvoie" cette nouvelle valeur. i ++, d'autre part, crée effectivement une variable temporaire pour contenir la valeur actuelle dans i, puis incrémente la variable "renvoyant" la valeur de la variable temporaire. C'est de là que viennent les frais généraux supplémentaires.
// i++ evaluates to something like this
// Imagine though that somehow i was passed by reference
int temp = i;
i = i + 1;
return temp;
// ++i evaluates to
i = i + 1;
return i;
Dans votre cas, il semble que l'incrément ne sera pas optimisé par la JVM car vous utilisez le résultat dans une expression. La JVM peut en revanche optimiser une boucle comme celle-ci.
for( int i = 0; i < Integer.MAX_VALUE; i++ ) {}
En effet, le résultat d'i ++ n'est jamais utilisé. Dans une boucle comme celle-ci, vous devriez pouvoir utiliser à la fois ++ i et i ++ avec les mêmes performances que si vous utilisiez ++ i.
EDIT 2
Vous devriez vraiment regarder ici:
[~ # ~] edit [~ # ~] Plus j'y pense, je me rends compte que ce test est en quelque sorte faux, la boucle deviendra sérieuse optimisé par la JVM.
Je pense que vous devriez simplement laisser tomber le @Param
et laissez n=2
.
De cette façon, vous testerez les performances du while
lui-même. Les résultats que j'obtiens dans ce cas:
o.m.t.WhileTest.testFirst avgt 5 0.787 0.086 ns/op
o.m.t.WhileTest.testSecond avgt 5 0.782 0.087 ns/op
Il n'y a presque aucune différence
La toute première question que vous devriez vous poser est comment vous testez et mesurez cela . Il s'agit d'un micro-benchmarking et en Java c'est un art, et presque toujours un simple utilisateur (comme moi) obtiendra des résultats erronés. Vous devriez vous fier à un test de référence et à un très bon outil pour J'ai utilisé JMH pour tester ceci:
@Measurement(iterations=5, time=1, timeUnit=TimeUnit.MILLISECONDS)
@Fork(1)
@Warmup(iterations=5, time=1, timeUnit=TimeUnit.SECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Benchmark)
public class WhileTest {
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(".*" + WhileTest.class.getSimpleName() + ".*")
.threads(1)
.build();
new Runner(opt).run();
}
@Param({"100", "10000", "100000", "1000000"})
private int n;
/*
@State(Scope.Benchmark)
public static class HOLDER_I {
int x;
}
*/
@Benchmark
public int testFirst(){
int i = 0;
while (++i < n) {
}
return i;
}
@Benchmark
public int testSecond(){
int i = 0;
while (i++ < n) {
}
return i;
}
}
Quelqu'un de plus expérimenté en JMH pourrait corriger ces résultats (j'espère vraiment!, Car je ne suis pas encore aussi polyvalent en JMH), mais les résultats montrent que la différence est sacrément petite:
Benchmark (n) Mode Samples Score Score error Units
o.m.t.WhileTest.testFirst 100 avgt 5 1.271 0.096 ns/op
o.m.t.WhileTest.testFirst 10000 avgt 5 1.319 0.125 ns/op
o.m.t.WhileTest.testFirst 100000 avgt 5 1.327 0.241 ns/op
o.m.t.WhileTest.testFirst 1000000 avgt 5 1.311 0.136 ns/op
o.m.t.WhileTest.testSecond 100 avgt 5 1.450 0.525 ns/op
o.m.t.WhileTest.testSecond 10000 avgt 5 1.563 0.479 ns/op
o.m.t.WhileTest.testSecond 100000 avgt 5 1.418 0.428 ns/op
o.m.t.WhileTest.testSecond 1000000 avgt 5 1.344 0.120 ns/op
Le champ Score est celui qui vous intéresse.
ce test n'est probablement pas suffisant pour tirer des conclusions mais je dirais que si c'est le cas, la JVM peut optimiser cette expression en changeant i ++ en ++ i car la valeur stockée d'i ++ (pré-valeur) n'est jamais utilisée dans cette boucle.