29 votes

L'entier Java ++i ne change pas de valeur

Je viens de réaliser ce simple "programme" :

public static void main(String[] args) {
        int i = 1;
        int k = 0;
        while (true) {
            if(++i==0) System.out.println("loop: " + ++k);
        }
    }

En exécutant ce programme, j'obtiens immédiatement la sortie :

(...)
loop: 881452
loop: 881453
loop: 881454
loop: 881455
loop: 881456
loop: 881457
loop: 881458
(...)

comme si i serait toujours égal à 0.

Et en fait, quand je débogue dans Eclipse en cas de suspension du programme, i serait toujours égal à zéro. En passant par la boucle, i s'incrémente, mais lors de la reprise et de la suspension du débogueur, i est de nouveau égal à 0.

Quand je change i trop long, lorsque je lance le programme, je dois attendre un certain temps avant de voir le premier message de l'application. loop: 1 . Dans le débogueur, lors de la mise en pause du programme, i incrémente : ce n'est pas 0, donc cela fonctionne comme il se doit.

Quel est le problème avec ++i comme un int ?

49voto

erickson Points 127945

Si vous continuez à incrémenter un type de nombre entier, il finira par déborder, devenant une grande valeur négative. Si vous continuez, il finira par redevenir 0, et le cycle se répétera.

Il existe des méthodes pratiques permettant d'éviter les débordements accidentels, telles que Math.addExact() mais ils ne sont normalement pas utilisés dans une boucle.


Je sais qu'elle est débordante. Ce qui m'intrigue, c'est qu'il déborde aussi vite. Et je trouve étrange qu'à chaque fois que je suspends le débogueur, i soit égal à 0.

Lorsque vous suspendez un thread en cours d'exécution, tenez compte de la possibilité que le thread se trouve dans un appel lent à println() que de traverser une énorme pile de code Java et OS natif, par rapport à la probabilité d'atterrir dans le test de votre boucle while, qui ne fait qu'incrémenter une variable locale. Il faut avoir la gâchette assez rapide pour voir autre chose que l'instruction print. Essayez plutôt de passer à travers.

Lorsque quelque chose se produit 4 milliards de fois d'affilée, il y a fort à parier que cela se produira la prochaine fois. La prédiction de branche sera utile dans tous les cas, et il est possible que votre runtime optimisé supprime entièrement l'opération d'incrémentation et le test, puisque les valeurs intermédiaires de i ne sont jamais lus.

22voto

Marco13 Points 14743

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...

11voto

mk. Points 620

Votre boucle est débordante i . Vous n'avez pas break donc après un certain temps, i revient à 0, et ceci imprime la déclaration et incrémente k . Cela explique également pourquoi la modification de la int à un long provoque un ralentissement de l'impression : il faut beaucoup plus de temps pour qu'une long pour déborder.

10voto

plugwash Points 795

Voyons d'abord ce que fait logiquement cette boucle.

i débordera à plusieurs reprises. Tous les 2 32 (environ 4 milliards) d'itérations de la boucle, la sortie sera imprimée et k sera incrémenté.

C'est le point de vue logique. Cependant, les compilateurs et les moteurs d'exécution sont autorisés à optimiser et si vous obtenez plus d'une valeur toutes les secondes environ, il est clair qu'une telle optimisation doit avoir lieu. Même avec la prédiction moderne des branchements, l'exécution hors ordre, etc. je trouve improbable qu'un CPU puisse contourner une boucle serrée plus d'une fois par cycle d'horloge (et même cela, je le considère comme improbable). Le fait que vous ne voyez jamais rien d'autre que zéro dans un débogueur renforce l'idée que le code est optimisé.

Vous mentionnez que le temps est plus long lorsque le compteur "long" est utilisé et que vous voyez d'autres valeurs. Si un compteur "long" était utilisé dans une boucle non optimisée, on pourrait s'attendre à de nombreuses décennies entre les valeurs. Encore une fois, il est clair que l'optimisation a lieu, mais il semble que l'optimiseur abandonne avant d'avoir complètement optimisé les itérations inutiles.

4voto

Erik Z Points 499

Il n'est pas toujours 0, il devient 0 après avoir bouclé (débordement d'entier), donc il deviendrait d'abord Integer.MAX_VALUE, puis Integer.MIN_VALUE, et ensuite remonterait jusqu'à 0. C'est pourquoi il semble qu'il soit toujours à 0, mais en fait il prend toutes les valeurs entières possibles avant de devenir 0... Encore et encore.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X