Die Ausgabe -1 wird zu einem Schrägstrich in der Schleife


54

Überraschenderweise wird der folgende Code ausgegeben:

/
-1

Der 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);
        }
    }

}

Ich habe viele Male versucht festzustellen, wie oft dies auftreten würde, aber leider war es letztendlich ungewiss, und ich stellte fest, dass die Ausgabe von -2 manchmal zu einer Periode wurde. Außerdem habe ich versucht, die while-Schleife zu entfernen und -1 ohne Probleme auszugeben. Wer kann mir sagen warum?


Informationen zur JDK-Version:

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1

2
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Samuel Liew

Antworten:


36

Dies kann zuverlässig (oder nicht reproduziert, je nachdem, was Sie wollen) mit openjdk version "1.8.0_222"(in meiner Analyse verwendet), OpenJDK 12.0.1(nach Oleksandr Pyrohov) und OpenJDK 13 (nach Carlos Heuberger) reproduziert werden.

Ich habe den Code mit -XX:+PrintCompilationgenügend Zeit ausgeführt, um beide Verhaltensweisen zu erhalten, und hier sind die Unterschiede.

Buggy-Implementierung (zeigt die Ausgabe an):

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

Richtiger Lauf (keine Anzeige):

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

Wir können einen signifikanten Unterschied feststellen. Bei korrekter Ausführung kompilieren wir test()zweimal. Einmal am Anfang und noch einmal danach (vermutlich, weil die JIT bemerkt, wie heiß die Methode ist). Im Buggy wird die Ausführung 5 mal test()kompiliert (oder dekompiliert) .

Wenn Sie mit -XX:-TieredCompilation(was entweder interpretiert oder verwendet C2) oder mit -Xbatch(was die Kompilierung zwingt, im Hauptthread statt parallel ausgeführt zu werden) ausgeführt werden, ist die Ausgabe garantiert und mit 30000 Iterationen wird eine Menge Material ausgedruckt, so dass der C2Compiler scheint der Schuldige sein. Dies wird durch Ausführen mit bestätigt -XX:TieredStopAtLevel=1, wodurch die C2Ausgabe deaktiviert und nicht erzeugt wird (das Anhalten auf Stufe 4 zeigt den Fehler erneut an).

Bei der korrekten Ausführung wird die Methode zuerst mit der Kompilierung der Ebene 3 und anschließend mit der Kompilierung der Ebene 4 kompiliert .

Bei der fehlerhaften Ausführung werden die vorherigen Kompilierungen verworfen ( made non entrant) und erneut auf Ebene 3 kompiliert ( C1siehe vorherigen Link).

Es ist also definitiv ein Fehler C2, obwohl ich nicht absolut sicher bin, ob die Tatsache, dass es zurück zur Level 3-Kompilierung geht, sich darauf auswirkt (und warum es zurück zu Level 3 geht, so viele Unsicherheiten immer noch).

Sie können den Baugruppencode mit der folgenden Zeile generieren, um noch tiefer in das Kaninchenloch zu gelangen (siehe auch dies , um das Drucken von Baugruppen zu ermöglichen).

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

An diesem Punkt gehen mir langsam die Fähigkeiten aus, das fehlerhafte Verhalten zeigt sich, wenn die vorherigen kompilierten Versionen verworfen werden, aber die wenigen Montagefähigkeiten, die ich habe, stammen aus den 90ern, also lasse ich jemanden schlauer als ich von hier.

Es ist wahrscheinlich, dass es bereits einen Fehlerbericht darüber gibt, da der Code dem OP von jemand anderem präsentiert wurde und da der gesamte Code C2 nicht ohne Fehler ist . Ich hoffe, diese Analyse war für andere genauso informativ wie für mich.

Wie der ehrwürdige Apangin in den Kommentaren hervorhob, ist dies ein neuer Fehler . Vielen Dank an alle interessierten und hilfsbereiten Leute :)


Ich denke auch, dass es C2- habe den generierten Assembler-Code mit JitWatch betrachtet (und versucht, ihn zu verstehen) - der C1generierte Code ähnelt immer noch dem Bytecode, C2ist völlig anders (ich konnte nicht einmal die Initialisierung von i8 finden)
user85421-Banned

Ihre Antwort ist sehr gut, ich habe versucht, c2 zu deaktivieren, das Ergebnis ist korrekt. Im Allgemeinen sind die meisten dieser Parameter jedoch im Projekt Standard, obwohl das eigentliche Projekt nicht über den obigen Code verfügt. Es ist jedoch wahrscheinlich, dass es einen ähnlichen Code enthält. Wenn das Projekt ähnlichen Code verwendet, ist dies wirklich schrecklich
okali

1
@ Eugene das war ziemlich knifflig, ich war mir sicher, dass es so etwas wie ein Eclipse-Compiler-Bug oder ähnliches werden würde ... und ich konnte es auch zuerst nicht reproduzieren.
Kayaman

1
@ Kayaman stimmte zu. Die Analyse, die Sie durchgeführt haben, ist sehr gut. Es sollte mehr als genug sein, damit Apangin dies erklären und beheben kann. Was für ein fabelhafter Morgen im Zug!
Eugene

7
Ich habe dieses Thema nur zufällig bemerkt. Verwenden Sie @mentions oder fügen Sie ein # jvm-Tag hinzu, um sicherzustellen, dass die Frage angezeigt wird. Gute Analyse, übrigens. Dies ist in der Tat ein C2-Compiler-Fehler, der erst vor wenigen Tagen behoben wurde - JDK-8231988 .
Apangin

4

Dies ist ehrlich gesagt ziemlich seltsam, da dieser Code technisch nie ausgegeben werden sollte, weil ...

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

... sollte immer izu -1(8 - 3 = 5; 5 - 3 = 2; 2 - 3 = -1) führen. Was noch seltsamer ist, ist, dass es niemals im Debug-Modus meiner IDE ausgegeben wird.

Interessanterweise ist der Moment, in dem ich vor der Konvertierung einen Scheck hinzufüge String, kein Problem ...

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);
  }
}

Nur zwei Punkte guter Codierungspraxis ...

  1. Eher benutzen String.valueOf()
  2. Einige Codierungsstandards legen fest, dass String-Literale eher das Ziel .equals()als das Argument sein sollen, wodurch NullPointerExceptions minimiert werden.

Die einzige Möglichkeit, dies nicht zu erreichen, war die Verwendung von 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);
  }
}

... im Grunde sieht es so aus, als ob Java ein bisschen Zeit braucht, um zu Atem zu kommen :)

BEARBEITEN: Dies mag völlig zufällig sein, aber es scheint eine gewisse Übereinstimmung zwischen dem auszudruckenden Wert und der ASCII-Tabelle zu bestehen .

  • i= -1, angezeigtes Zeichen ist /(ASCII-Dezimalwert von 47)
  • i= -2, angezeigtes Zeichen ist .(ASCII-Dezimalwert von 46)
  • i= -3, angezeigtes Zeichen ist -(ASCII-Dezimalwert von 45)
  • i= -4, angezeigtes Zeichen ist ,(ASCII-Dezimalwert von 44)
  • i= -5, angezeigtes Zeichen ist +(ASCII-Dezimalwert von 43)
  • i= -6, angezeigtes Zeichen ist *(ASCII-Dezimalwert von 42)
  • i= -7, angezeigtes Zeichen ist )(ASCII-Dezimalwert von 41)
  • i= -8, angezeigtes Zeichen ist ((ASCII-Dezimalwert von 40)
  • i= -9, angezeigtes Zeichen ist '(ASCII-Dezimalwert von 39)

Was wirklich interessant ist, ist, dass das Zeichen bei ASCII-Dezimalzahl 48 der Wert ist 0und 48 - 1 = 47 (Zeichen /) usw.


1
Der numerische Wert des Zeichens "/" ist "-1" ??? woher kommt das? ( (int)'/' == 47; (char)-1ist undefiniert 0xFFFFist <kein Zeichen> in Unicode)
user85421-Banned

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

Wie hängt das getNumericValue()mit dem gegebenen Code zusammen? und wie konvertiert es -1zu '/'??? Warum nicht '-', getNumericValue('-')ist auch -1??? ( -1
Übrigens

@CarlosHeuberger, ich lief getNumericValue()auf value( /), um den Zeichenwert zu erhalten. Sie sind zu 100% richtig, dass der ASCII-Dezimalwert von /47 sein sollte (es war das, was ich auch erwartet hatte), aber getNumericValue()zu diesem Zeitpunkt -1 zurückgegeben hat, wie ich hinzugefügt hatte System.out.println(Character.getNumericValue(value.toCharArray()[0]));. Ich kann die Verwirrung sehen, auf die Sie sich beziehen, und habe den Beitrag aktualisiert.
Ambro-r

1

Ich weiß nicht, warum Java eine so zufällige Ausgabe liefert, aber das Problem liegt in Ihrer Verkettung, die bei größeren Werten iinnerhalb der forSchleife fehlschlägt .

Wenn Sie die String value = i + "";Zeile durch String value = String.valueOf(i) ;Ihren Code ersetzen , funktioniert dies wie erwartet.

Die Verkettung +zum Konvertieren des int in einen String ist nativ und möglicherweise fehlerhaft (seltsamerweise gründen wir sie jetzt wahrscheinlich) und verursacht ein solches Problem.

Hinweis: Ich habe den Wert von i inside for loop auf 10000 reduziert und hatte kein Problem mit der +Verkettung.

Dieses Problem muss den Java-Stakeholdern gemeldet werden und sie können ihre Meinung dazu abgeben.

Bearbeiten Ich habe den Wert von i in for loop auf 3 Millionen aktualisiert und eine neue Reihe von Fehlern wie folgt gesehen:

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)

Meine Java-Version ist 8.


1
Ich denke nicht, dass String-Verkettung nativ ist - es verwendet nur StringConcatFactory(OpenJDK 13) oder StringBuilder(Java 8)
user85421-Banned

@CarlosHeuberger Auch möglich. Ich denke, es ist aus Java 9, wenn es StringConcatFactory Klasse sein muss. aber soweit ich weiß, Java bis Java 8 Java nicht unterstützen Operator Überladung
Vinay Prajapati

@ Vinay, habe dies auch versucht und ja, es funktioniert, aber sobald Sie die Schleife von 30000 auf 3000000 erhöhen, tritt das gleiche Problem auf.
Ambro-r

@ Ambro-r Ich habe es mit deinem vorgeschlagenen Wert versucht und bekomme eine Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1Fehlermeldung. Seltsam.
Vinay Prajapati

3
i + ""wird genau wie new StringBuilder().append(i).append("").toString()in Java 8 kompiliert , und wenn Sie dies verwenden, wird schließlich auch die Ausgabe erzeugt
user85421-Banned
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.