É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
Réponses:
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), OpenJDK12.0.1
(selon Oleksandr Pyrohov) et OpenJDK 13 (selon Carlos Heuberger).J'ai exécuté le code avec
-XX:+PrintCompilation
suffisamment de temps pour obtenir les deux comportements et voici les différences.Implémentation du buggy (affiche la sortie):
Exécution correcte (pas d'affichage):
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écutiontest()
est compilée (ou décompilée) 5 fois.De plus, en exécutant avec
-XX:-TieredCompilation
(qui interprète ou utiliseC2
) 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 leC2
compilateur semble être le coupable. Ceci est confirmé par l'exécution de-XX:TieredStopAtLevel=1
, qui désactiveC2
et 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-à-direC1
, 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).
À 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 :)
la source
C2
- j'ai regardé le code assembleur généré (et essayé de le comprendre) en utilisant JitWatch - leC1
code généré ressemble toujours au bytecode,C2
est totalement différent (je n'ai même pas pu trouver l'initialisationi
avec 8)C'est honnêtement assez étrange, car ce code ne devrait techniquement jamais sortir parce que ...
... 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 ...Juste deux points de bonnes pratiques de codage ...
String.valueOf()
.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()
... 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
0
et 48 - 1 = 47 (caractère/
), etc ...la source
(int)'/' == 47
;(char)-1
n'est pas défini0xFFFF
n'est <pas un caractère> en Unicode)getNumericValue()
rapporte-t-il au code donné ??? et comment se convertit-il-1
en'/'
??? Pourquoi pas'-'
,getNumericValue('-')
c'est aussi-1
??? (BTW, beaucoup de méthodes reviennent-1
)getNumericValue()
survalue
(/
) 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), maisgetNumericValue()
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.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'
i
intérieur dufor
boucle.Si vous remplacez la
String value = i + "";
ligne parString 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:
Ma version Java est 8.
la source
StringConcatFactory
(OpenJDK 13) ouStringBuilder
(Java 8)StringConcatFactory
classe. mais pour autant que je sache, java jusqu'à java 8 java don; t supporte la surcharge de l'opérateurException in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
erreur. Étrange.i + ""
est compilé exactement commenew StringBuilder().append(i).append("").toString()
dans Java 8, et son utilisation finit également par produire la sortie