Wyjście -1 staje się ukośnikiem w pętli

54

Niespodziewanie następujące kody wyjściowe:

/
-1

Kod:

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

}

Wielokrotnie próbowałem ustalić, ile razy miałoby to nastąpić, ale niestety ostatecznie było to niepewne i stwierdziłem, że wyjście -2 czasami zamieniało się w kropkę. Ponadto próbowałem również usunąć pętlę while i wyjście -1 bez żadnych problemów. Kto może mi powiedzieć dlaczego?


Informacje o wersji JDK:

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1
okali
źródło
2
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Samuel Liew

Odpowiedzi:

36

Można to rzetelnie odtworzyć (lub nie odtworzyć, w zależności od tego, czego chcesz) za pomocą openjdk version "1.8.0_222"(zastosowanego w mojej analizie), OpenJDK 12.0.1(według Oleksandra Pyrohova) i OpenJDK 13 (według Carlosa Heubergera).

Uruchomiłem kod z -XX:+PrintCompilationwystarczającą ilością razy, aby uzyskać oba zachowania, a oto różnice.

Implementacja błędna (wyświetla dane wyjściowe):

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

Prawidłowy przebieg (bez wskazania):

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

Możemy zauważyć jedną istotną różnicę. Przy poprawnym wykonaniu kompilujemy test()dwa razy. Raz na początku i jeszcze raz później (prawdopodobnie dlatego, że JIT zauważa, jak gorąca jest ta metoda). W buggy wykonanie test()jest kompilowane (lub dekompilowane) 5 razy.

Dodatkowo, uruchamianie z -XX:-TieredCompilation(który interpretuje lub używa C2) lub z -Xbatch(co zmusza kompilację do uruchamiania w głównym wątku, zamiast równolegle), wyjście jest gwarantowane, a przy 30000 iteracjach drukuje wiele rzeczy, więc C2kompilator wydaje się być winowajcą. Potwierdza to uruchomienie z -XX:TieredStopAtLevel=1, które wyłącza C2i nie generuje danych wyjściowych (zatrzymanie na poziomie 4 ponownie pokazuje błąd).

W prawidłowym wykonaniu metoda jest najpierw kompilowana z kompilacją poziomu 3 , a następnie z poziomem 4.

W wykonaniu buggy poprzednie kompilacje są ignorowane ( made non entrant) i ponownie kompilowane na poziomie 3 (czyli C1patrz poprzedni link).

Więc na pewno jest to błąd C2, chociaż nie jestem absolutnie pewien, czy wpływa na to fakt, że wraca do kompilacji poziomu 3 (i dlaczego wraca do poziomu 3, wciąż jest tak wiele niepewności).

Można wygenerować kod montażowej z następującą linię iść nawet głębiej do króliczej nory (patrz również to , aby umożliwić montaż drukowania).

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

W tym momencie zaczynam brakować umiejętności, zachowanie buggy zaczyna się ujawniać, gdy poprzednie kompilowane wersje są odrzucane, ale to, co mam małe umiejętności montażowe, mam z lat 90-tych, więc pozwolę, by ktoś mądrzejszy ode mnie wziął stąd.

Prawdopodobnie jest już raport o błędzie na ten temat, ponieważ kod został przedstawiony OP przez kogoś innego, a ponieważ cały kod C2 nie jest bez błędów . Mam nadzieję, że ta analiza była tak samo pouczająca dla innych, jak dla mnie.

Jak zauważył czcigodny apangin w komentarzach, jest to ostatni błąd . Bardzo zobowiązany do wszystkich zainteresowanych i pomocnych osób :)

Kayaman
źródło
Myślę też, że jest C2- spojrzałem na wygenerowany kod asemblera (i spróbowałem go zrozumieć) za pomocą JitWatch - C1wygenerowany kod nadal przypomina kod bajtowy, C2jest zupełnie inny (nie mogłem nawet znaleźć inicjalizacji iz 8)
user85421-Zbanowany
twoja odpowiedź jest bardzo dobra, próbowałem, wyłączyć c2, wynik jest poprawny. Jednak generalnie większość z tych parametrów jest domyślna w projekcie, chociaż rzeczywisty projekt nie będzie miał powyższego kodu, ale prawdopodobnie będzie miał podobny kod, jeśli projekt używa podobnego kodu, to jest naprawdę okropne
okali
1
@Eugene było to dość trudne, byłem pewien, że będzie to coś w rodzaju błędu kompilatora zaćmienia lub podobnego ... i na początku nie mogłem go odtworzyć ..
Kayaman
1
@Kayaman zgodził się. Przeprowadzona analiza jest bardzo dobra, powinna wystarczyć, aby apangin mógł to wyjaśnić i naprawić. Co za wspaniały poranek w pociągu!
Eugene
7
Zauważyłem ten temat tylko przypadkowo. Aby upewnić się, że widzę pytanie, użyj @mentions lub dodaj tag #jvm. Dobra analiza, BTW. Jest to rzeczywiście błąd kompilatora C2, naprawiony zaledwie kilka dni temu - JDK-8231988 .
apangin
4

To jest naprawdę dość dziwne, ponieważ ten kod technicznie nigdy nie powinien być generowany, ponieważ ...

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

... powinno zawsze skutkować ibyciem -1(8 - 3 = 5; 5 - 3 = 2; 2 - 3 = -1). Jeszcze dziwniejsze jest to, że nigdy nie wychodzi ono w trybie debugowania mojego IDE.

Co ciekawe, w momencie, gdy dodam czek przed konwersją na String, to nie ma problemu ...

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

Tylko dwa punkty dobrej praktyki kodowania ...

  1. Raczej użyć String.valueOf()
  2. Niektóre standardy kodowania określają, że literały łańcuchowe powinny być celem .equals(), a nie argumentem, minimalizując w ten sposób wyjątki NullPointerExceptions.

Jedynym sposobem, aby to się nie wydarzyło, było użycie 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);
  }
}

... w zasadzie wygląda na to, że Java potrzebuje trochę czasu, aby złapać oddech :)

EDYCJA: Może to być całkowicie przypadkowa, ale wydaje się, że istnieje pewna zgodność między wartością, która jest drukowana, a tabelą ASCII .

  • i= -1, wyświetlany znak to /(wartość dziesiętna ASCII 47)
  • i= -2, wyświetlany znak to .(wartość dziesiętna ASCII 46)
  • i= -3, wyświetlany znak to -(wartość dziesiętna ASCII 45)
  • i= -4, wyświetlany znak to ,(wartość dziesiętna ASCII 44)
  • i= -5, wyświetlany znak to +(wartość dziesiętna ASCII 43)
  • i= -6, wyświetlany znak to *(wartość dziesiętna ASCII 42)
  • i= -7, wyświetlany znak to )(wartość dziesiętna ASCII 41)
  • i= -8, wyświetlany znak to ((wartość dziesiętna ASCII 40)
  • i= -9, wyświetlany znak to '(wartość dziesiętna ASCII 39)

Naprawdę interesujące jest to, że znak w postaci dziesiętnej 48 ASCII jest wartością, 0a 48 - 1 = 47 (znak /) itp.

Ambro-r
źródło
1
wartość liczbowa znaku „/” to „-1” ??? Skąd to pochodzi? ( (int)'/' == 47; (char)-1jest niezdefiniowany 0xFFFFto <nie znak> w Unicode)
user85421-Zbanowany
1
char c = „/”; int a = Character.getNumericValue (c); System.out.println (a);
Ambro-r
jak getNumericValue()odnosi się do danego kodu? i jak przekształca -1się w '/'??? Dlaczego nie '-', getNumericValue('-')jest też -1??? (BTW zwraca wiele metod -1)
użytkownik85421-Zbanował
@CarlosHeuberger, działałem getNumericValue()na value( /), aby uzyskać wartość znaku. Masz 100% rację, że wartość dziesiętna ASCII /powinna wynosić 47 (tego też się spodziewałam), ale getNumericValue()zwracała -1 w tym momencie, jak dodałem System.out.println(Character.getNumericValue(value.toCharArray()[0]));. Widzę zamieszanie, o którym mówisz, i zaktualizowałem post.
Ambro-r
1

Nie wiem, dlaczego Java daje tak losowe dane wyjściowe, ale problem polega na tym, że konkatenacja kończy się niepowodzeniem w przypadku większych wartości iwewnątrz forpętli.

Jeśli zastąpisz String value = i + "";wiersz String value = String.valueOf(i) ;kodem, zadziała zgodnie z oczekiwaniami.

Łączenie za pomocą +konwersji int na ciąg znaków jest rodzime i może być wadliwe (prawdopodobnie dziwnie go teraz znajdujemy) i powoduje taki problem.

Uwaga: Zmniejszyłem wartość i inside for loop do 10000 i nie napotkałem problemu z +konkatenacją.

Ten problem należy zgłosić interesariuszom Java i mogą wyrazić swoją opinię na ten temat.

Edytuj Zaktualizowałem wartość i dla pętli do 3 milionów i zobaczyłem nowy zestaw błędów, jak poniżej:

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)

Moja wersja Java to 8.

Vinay Prajapati
źródło
1
Nie sądzę, że konkatenacja łańcuchów jest natywna - po prostu używa StringConcatFactory(OpenJDK 13) lub StringBuilder(Java 8)
user85421-Zbanowany
@CarlosHeuberger Możliwe też. Myślę, że pochodzi z Java 9, jeśli to musi być StringConcatFactory klasa. ale o ile wiem, java do java 8 java; t wspiera przeciążanie operatora
Vinay Prajapati
@Vinay, wypróbowałem to również i tak, działa, ale w momencie, gdy zwiększysz pętlę z 30000 do 3000000, zaczniesz mieć ten sam problem.
Ambro-r
@ Ambro-r Próbowałem z twoją sugerowaną wartością i dostaję Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1błąd. Dziwne.
Vinay Prajapati,
3
i + ""jest skompilowany dokładnie tak, jak new StringBuilder().append(i).append("").toString()w Javie 8, a użycie tego ostatecznie generuje również wynik
user85421-Zbanowany