La sortie -1 devient une barre oblique dans la boucle


54

Étonnamment, le code suivant sort:

/
-1

Le code:

public class LoopOutPut {

    public static void main(String[] args) {
        LoopOutPut loopOutPut = new LoopOutPut();
        for (int i = 0; i < 30000; i++) {
            loopOutPut.test();
        }

    }

    public void test() {
        int i = 8;
        while ((i -= 3) > 0) ;
        String value = i + "";
        if (!value.equals("-1")) {
            System.out.println(value);
            System.out.println(i);
        }
    }

}

J'ai essayé à plusieurs reprises de déterminer combien de fois cela se produirait, mais, malheureusement, c'était finalement incertain, et j'ai trouvé que la sortie de -2 se transformait parfois en période. De plus, j'ai également essayé de supprimer la boucle while et la sortie -1 sans aucun problème. Qui peut me dire pourquoi?


Informations sur la version JDK:

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1

2
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew

Réponses:


36

Cela peut être reproduit de manière fiable (ou non reproduit, selon ce que vous voulez) avec openjdk version "1.8.0_222"(utilisé dans mon analyse), OpenJDK 12.0.1(selon Oleksandr Pyrohov) et OpenJDK 13 (selon Carlos Heuberger).

J'ai exécuté le code avec -XX:+PrintCompilationsuffisamment de temps pour obtenir les deux comportements et voici les différences.

Implémentation du buggy (affiche la sortie):

 --- Previous lines are identical in both
 54   17       3       java.lang.AbstractStringBuilder::<init> (12 bytes)
 54   23       3       LoopOutPut::test (57 bytes)
 54   18       3       java.lang.String::<init> (82 bytes)
 55   21       3       java.lang.AbstractStringBuilder::append (62 bytes)
 55   26       4       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
 55   20       3       java.lang.StringBuilder::<init> (7 bytes)
 56   19       3       java.lang.StringBuilder::toString (17 bytes)
 56   25       3       java.lang.Integer::getChars (131 bytes)
 56   22       3       java.lang.StringBuilder::append (8 bytes)
 56   27       4       java.lang.String::equals (81 bytes)
 56   10       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   made not entrant
 56   28       4       java.lang.AbstractStringBuilder::append (50 bytes)
 56   29       4       java.lang.String::getChars (62 bytes)
 56   24       3       java.lang.Integer::stringSize (21 bytes)
 58   14       3       java.lang.String::getChars (62 bytes)   made not entrant
 58   33       4       LoopOutPut::test (57 bytes)
 59   13       3       java.lang.AbstractStringBuilder::append (50 bytes)   made not entrant
 59   34       4       java.lang.Integer::getChars (131 bytes)
 60    3       3       java.lang.String::equals (81 bytes)   made not entrant
 60   30       4       java.util.Arrays::copyOfRange (63 bytes)
 61   25       3       java.lang.Integer::getChars (131 bytes)   made not entrant
 61   32       4       java.lang.String::<init> (82 bytes)
 61   16       3       java.util.Arrays::copyOfRange (63 bytes)   made not entrant
 61   31       4       java.lang.AbstractStringBuilder::append (62 bytes)
 61   23       3       LoopOutPut::test (57 bytes)   made not entrant
 61   33       4       LoopOutPut::test (57 bytes)   made not entrant
 62   35       3       LoopOutPut::test (57 bytes)
 63   36       4       java.lang.StringBuilder::append (8 bytes)
 63   18       3       java.lang.String::<init> (82 bytes)   made not entrant
 63   38       4       java.lang.StringBuilder::append (8 bytes)
 64   21       3       java.lang.AbstractStringBuilder::append (62 bytes)   made not entrant

Exécution correcte (pas d'affichage):

 --- Previous lines identical in both
 55   23       3       LoopOutPut::test (57 bytes)
 55   17       3       java.lang.AbstractStringBuilder::<init> (12 bytes)
 56   18       3       java.lang.String::<init> (82 bytes)
 56   20       3       java.lang.StringBuilder::<init> (7 bytes)
 56   21       3       java.lang.AbstractStringBuilder::append (62 bytes)
 56   26       4       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)
 56   19       3       java.lang.StringBuilder::toString (17 bytes)
 57   22       3       java.lang.StringBuilder::append (8 bytes)
 57   24       3       java.lang.Integer::stringSize (21 bytes)
 57   25       3       java.lang.Integer::getChars (131 bytes)
 57   27       4       java.lang.String::equals (81 bytes)
 57   28       4       java.lang.AbstractStringBuilder::append (50 bytes)
 57   10       3       java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes)   made not entrant
 57   29       4       java.util.Arrays::copyOfRange (63 bytes)
 60   16       3       java.util.Arrays::copyOfRange (63 bytes)   made not entrant
 60   13       3       java.lang.AbstractStringBuilder::append (50 bytes)   made not entrant
 60   33       4       LoopOutPut::test (57 bytes)
 60   34       4       java.lang.Integer::getChars (131 bytes)
 61    3       3       java.lang.String::equals (81 bytes)   made not entrant
 61   32       4       java.lang.String::<init> (82 bytes)
 62   25       3       java.lang.Integer::getChars (131 bytes)   made not entrant
 62   30       4       java.lang.AbstractStringBuilder::append (62 bytes)
 63   18       3       java.lang.String::<init> (82 bytes)   made not entrant
 63   31       4       java.lang.String::getChars (62 bytes)

Nous pouvons remarquer une différence significative. Avec la bonne exécution, nous compilons test()deux fois. Une fois au début, et encore une fois après (probablement parce que le JIT remarque à quel point la méthode est chaude). Dans le buggy, l'exécution test()est compilée (ou décompilée) 5 fois.

De plus, en exécutant avec -XX:-TieredCompilation(qui interprète ou utilise C2) ou avec -Xbatch(qui force la compilation à s'exécuter dans le thread principal, au lieu de parallèlement), la sortie est garantie et avec 30000 itérations imprime beaucoup de choses, donc le C2compilateur semble être le coupable. Ceci est confirmé par l'exécution de -XX:TieredStopAtLevel=1, qui désactive C2et ne produit pas de sortie (l'arrêt au niveau 4 montre à nouveau le bogue).

Dans l'exécution correcte, la méthode est d'abord compilée avec la compilation de niveau 3 , puis avec le niveau 4.

Dans l'exécution du buggy, les compilations précédentes sont supprimées ( made non entrant) et elles sont à nouveau compilées au niveau 3 (c'est-à-dire C1, voir le lien précédent).

Il s'agit donc définitivement d'un bug C2, même si je ne suis pas absolument sûr de savoir si le fait de revenir à la compilation de niveau 3 l'affecte (et pourquoi revient-il au niveau 3, tant d'incertitudes encore).

Vous pouvez générer le code d'assemblage avec la ligne suivante pour aller encore plus loin dans le trou du lapin (voir également ceci pour activer l'impression d'assemblage).

java -XX:+PrintCompilation -Xbatch -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly LoopOutPut > broken.asm

À ce stade, je commence à manquer de compétences, le comportement du buggy commence à se manifester lorsque les versions compilées précédentes sont supprimées, mais le peu de compétences d'assemblage que j'ai des années 90, donc je vais laisser quelqu'un plus intelligent que moi le prendre d'ici.

Il est probable qu'il existe déjà un rapport de bogue à ce sujet, car le code a été présenté à l'OP par quelqu'un d'autre, et comme tout le code C2 n'est pas sans bogues . J'espère que cette analyse a été aussi informative pour les autres que pour moi.

Comme l'a souligné le vénérable apangin dans les commentaires, il s'agit d'un bug récent . Je suis très obligé envers toutes les personnes intéressées et utiles :)


Je pense aussi que c'est C2- j'ai regardé le code assembleur généré (et essayé de le comprendre) en utilisant JitWatch - le C1code généré ressemble toujours au bytecode, C2est totalement différent (je n'ai même pas pu trouver l'initialisation iavec 8)
user85421-Banned

ta réponse est très bonne, j'ai essayé de désactiver c2, le résultat est correct. Cependant, en général, la plupart de ces paramètres sont par défaut dans le projet, bien que le projet réel n'ait pas le code ci-dessus, mais il est susceptible d'avoir un code similaire, si le projet utilise un code similaire, c'est vraiment terrible
okali

1
@Eugene, cela a été assez délicat, j'étais sûr que ça allait être quelque chose comme un bug du compilateur d'éclipse ou similaire ... et je ne pouvais pas le reproduire au début non plus.
Kayaman

1
@Kayaman a accepté. L'analyse que vous avez faite est très bonne, cela devrait être plus que suffisant pour que l'apangin explique et corrige cela. Quelle fabuleuse matinée dans le train!
Eugene

7
J'ai remarqué ce sujet accidentellement. Pour être sûr de voir la question, utilisez @mentions ou ajoutez une balise #jvm. Bonne analyse, BTW. Il s'agit en effet d'un bug du compilateur C2, corrigé il y a seulement quelques jours - JDK-8231988 .
Apangin

4

C'est honnêtement assez étrange, car ce code ne devrait techniquement jamais sortir parce que ...

int i = 8;
while ((i -= 3) > 0);

... devrait toujours entraîner i être -1(8 - 3 = 5; 5 - 3 = 2; 2 - 3 = -1). Ce qui est encore plus étrange, c'est qu'il ne sort jamais en mode débogage de mon IDE.

Fait intéressant, au moment où j'ajoute un chèque avant la conversion en un String, alors aucun problème ...

public void test() {
  int i = 8;
  while ((i -= 3) > 0);
  if(i != -1) { System.out.println("Not -1"); }
  String value = String.valueOf(i);
  if (!"-1".equalsIgnoreCase(value)) {
    System.out.println(value);
    System.out.println(i);
  }
}

Juste deux points de bonnes pratiques de codage ...

  1. Plutôt utiliser String.valueOf()
  2. Certaines normes de codage spécifient que les littéraux de chaîne doivent être la cible de .equals() plutôt que l'argument, minimisant ainsi les NullPointerExceptions.

La seule façon pour que cela ne se produise pas était d'utiliser String.format()

public void test() {
  int i = 8;
  while ((i -= 3) > 0);
  String value = String.format("%d", i);
  if (!"-1".equalsIgnoreCase(value)) {
    System.out.println(value);
    System.out.println(i);
  }
}

... il semble que Java ait besoin d'un peu de temps pour reprendre son souffle :)

EDIT: Cela peut être complètement une coïncidence, mais il semble y avoir une certaine correspondance entre la valeur imprimée et la table ASCII .

  • i= -1, le caractère affiché est/ (valeur décimale ASCII de 47)
  • i= -2, le caractère affiché est .(valeur décimale ASCII de 46)
  • i= -3, le caractère affiché est -(valeur décimale ASCII de 45)
  • i= -4, le caractère affiché est ,(valeur décimale ASCII de 44)
  • i= -5, le caractère affiché est +(valeur décimale ASCII de 43)
  • i= -6, le caractère affiché est *(valeur décimale ASCII de 42)
  • i= -7, le caractère affiché est )(valeur décimale ASCII de 41)
  • i= -8, le caractère affiché est ((valeur décimale ASCII de 40)
  • i= -9, le caractère affiché est '(valeur décimale ASCII de 39)

Ce qui est vraiment intéressant, c'est que le caractère en ASCII 48 décimal est la valeur 0et 48 - 1 = 47 (caractère /), etc ...


1
la valeur numérique du caractère "/" est "-1" ??? D'où est-ce que ça vient? ( (int)'/' == 47; (char)-1n'est pas défini 0xFFFFn'est <pas un caractère> en Unicode)
user85421-Banned

1
char c = '/'; int a = Character.getNumericValue (c); System.out.println (a);
Ambro-r

comment se getNumericValue()rapporte-t-il au code donné ??? et comment se convertit-il -1en '/'??? Pourquoi pas '-', getNumericValue('-')c'est aussi -1??? (BTW, beaucoup de méthodes reviennent -1)
user85421-Banned

@CarlosHeuberger, je courais getNumericValue()sur value( /) pour obtenir la valeur du caractère. Vous avez 100% raison de dire que la valeur décimale ASCII de /devrait être 47 (c'était ce que j'attendais également), mais getNumericValue()retournait -1 à ce moment-là comme je l'avais ajouté System.out.println(Character.getNumericValue(value.toCharArray()[0]));. Je peux voir la confusion à laquelle vous faites référence et j'ai mis à jour le message.
Ambro-r

1

Je ne sais pas pourquoi Java donne une telle sortie aléatoire, mais le problème est dans votre concaténation qui échoue pour les valeurs plus grandes de l' iintérieur dufor boucle.

Si vous remplacez la String value = i + "";ligne par String value = String.valueOf(i) ;votre code fonctionne comme prévu.

La concaténation utilisant +pour convertir l'int en chaîne est native et peut être boguée (bizarrement, nous la trouvons maintenant probablement) et provoquant un tel problème.

Remarque: j'ai réduit la valeur de i inside for loop à 10000 et je n'ai pas rencontré de problème de +concaténation.

Ce problème doit être signalé aux parties prenantes Java et ils peuvent donner leur avis à ce sujet.

Modifier J'ai mis à jour la valeur de i in for loop à 3 millions et j'ai vu un nouvel ensemble d'erreurs comme ci-dessous:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
    at java.lang.Integer.getChars(Integer.java:463)
    at java.lang.Integer.toString(Integer.java:402)
    at java.lang.String.valueOf(String.java:3099)
    at solving.LoopOutPut.test(LoopOutPut.java:16)
    at solving.LoopOutPut.main(LoopOutPut.java:8)

Ma version Java est 8.


1
Je ne pense pas que la concaténation de chaînes soit native - elle utilise simplement StringConcatFactory(OpenJDK 13) ou StringBuilder(Java 8)
user85421-Banned

@CarlosHeuberger Possible aussi. Je pense que ça vient de Java 9 si ça doit être de la StringConcatFactory classe. mais pour autant que je sache, java jusqu'à java 8 java don; t supporte la surcharge de l'opérateur
Vinay Prajapati

@Vinay, a également essayé et oui, cela fonctionne, mais au moment où vous augmentez la boucle de 30000 à 3000000, vous commencez à avoir le même problème.
Ambro-r

@ Ambro-r J'ai essayé avec votre valeur suggérée et je reçois une Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1erreur. Étrange.
Vinay Prajapati

3
i + ""est compilé exactement comme new StringBuilder().append(i).append("").toString()dans Java 8, et son utilisation finit également par produire la sortie
user85421-Banned
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.