web-dev-qa-db-fra.com

Pourquoi "while (i ++ <n) {}" est beaucoup plus lent que "while (++ i <n) {}"

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.

74
sikan

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} &apos;runPreIncrement&apos; &apos;()V&apos; in &apos;PrePostIncrement&apos;
  #           [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} &apos;runPostIncrement&apos; &apos;()V&apos; in &apos;PrePostIncrement&apos;
  #           [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 ...

118
Marco13

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.

19
Smith_61

EDIT 2

Vous devriez vraiment regarder ici:

http://hg.openjdk.Java.net/code-tools/jmh/file/f90aef7f1d2c/jmh-samples/src/main/Java/org/openjdk/jmh/samples/JMHSample_11_Loops.Java

[~ # ~] 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.

18
Eugene

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.

0
danibuiza