L'output -1 diventa una barra nel ciclo


54

Sorprendentemente, il seguente codice genera:

/
-1

Il codice:

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

}

Ho provato molte volte a determinare quante volte ciò sarebbe accaduto, ma, sfortunatamente, alla fine era incerto e ho scoperto che l'output di -2 a volte si è trasformato in un periodo. Inoltre, ho anche provato a rimuovere il ciclo while e l'output -1 senza problemi. Chi può dirmi perché?


Informazioni sulla versione JDK:

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1

2
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew

Risposte:


36

Questo può essere riprodotto in modo affidabile (o non riprodotto, a seconda di ciò che si desidera) con openjdk version "1.8.0_222"(utilizzato nella mia analisi), OpenJDK 12.0.1(secondo Oleksandr Pyrohov) e OpenJDK 13 (secondo Carlos Heuberger).

Ho eseguito il codice con -XX:+PrintCompilationtempi sufficienti per ottenere entrambi i comportamenti e qui ci sono le differenze.

Implementazione con errori (visualizza l'output):

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

Esecuzione corretta (nessuna visualizzazione):

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

Possiamo notare una differenza significativa. Con l'esecuzione corretta compiliamo test()due volte. Una volta all'inizio, e ancora una volta in seguito (presumibilmente perché la JIT nota quanto sia caldo il metodo). Nell'esecuzione con buggy test()viene compilata (o decompilata) 5 volte.

Inoltre, in esecuzione con -XX:-TieredCompilation(che interpreta o utilizza C2) o con -Xbatch(che forza l'esecuzione della compilazione nel thread principale, anziché in parallelo), l'output è garantito e con 30000 iterazioni stampa un sacco di cose, quindi il C2compilatore sembra essere il colpevole. Ciò è confermato eseguendo con -XX:TieredStopAtLevel=1, che disabilita C2e non produce output (l'arresto al livello 4 mostra nuovamente il bug).

Nell'esecuzione corretta, il metodo viene prima compilato con la compilazione di livello 3 , quindi successivamente con il livello 4.

Nell'esecuzione con buggy, le compilazioni precedenti vengono scartate ( made non entrant) ed è di nuovo compilata al livello 3 (che è C1, vedi link precedente).

Quindi è sicuramente un bug C2, anche se non sono assolutamente sicuro che il fatto che stia tornando alla compilazione di Livello 3 lo influenzi (e perché sta tornando al livello 3, tante incertezze ancora).

È possibile generare il codice dell'assieme con la seguente riga per approfondire ulteriormente la tana del coniglio (vedere anche questo per abilitare la stampa dell'assieme).

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

A questo punto sto iniziando a rimanere senza abilità, il comportamento buggy inizia a manifestarsi quando le precedenti versioni compilate vengono scartate, ma quali piccole abilità di assemblaggio ho degli anni '90, quindi lascerò che qualcuno più intelligente di me lo prenda da qui.

È probabile che ci sia già una segnalazione di bug al riguardo, poiché il codice è stato presentato all'OP da qualcun altro e poiché tutto il codice C2 non è privo di bug . Spero che questa analisi sia stata tanto istruttiva per gli altri quanto lo è stata per me.

Come ha sottolineato il venerabile apangin nei commenti, si tratta di un bug recente . Molto obbligato a tutte le persone interessate e disponibili :)


Penso anche che sia C2- ho guardato il codice assembler generato (e ho provato a capirlo) usando JitWatch - il C1codice generato assomiglia ancora al bytecode, C2è totalmente diverso (non riuscivo nemmeno a trovare l'inizializzazione icon 8)
user85421-Banned

la tua risposta è molto buona, ho provato, disabilita c2, il risultato è corretto. Tuttavia, in generale, la maggior parte di questi parametri sono predefiniti nel progetto, anche se il progetto effettivo non avrà il codice sopra, ma è probabile che abbia un codice simile, se il progetto utilizza un codice simile, è davvero terribile
okali

1
@Eugene questo è stato piuttosto complicato, ero sicuro che sarebbe stato qualcosa di simile al bug del compilatore di eclissi o simile ... e non potevo nemmeno riprodurlo all'inizio ..
Kayaman,

1
@Kayaman ha concordato. L'analisi che hai fatto è molto buona, dovrebbe essere più che sufficiente per spiegare e risolvere questo problema. Che favolosa mattina sul treno!
Eugene,

7
Ho notato questo argomento solo per caso. Per essere sicuro di vedere la domanda, usa @mentions o aggiungi un tag #jvm. Buona analisi, a proposito. Questo è davvero un bug del compilatore C2, corretto solo pochi giorni fa - JDK-8231988 .
aprile

4

Questo è onestamente abbastanza strano, dal momento che quel codice non dovrebbe tecnicamente mai essere emesso perché ...

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

... dovrebbe sempre risultare in iessere -1(8 - 3 = 5; 5 - 3 = 2; 2 - 3 = -1). Ciò che è ancora più strano è che non esce mai nella modalità debug del mio IDE.

È interessante notare che nel momento in cui aggiungo un controllo prima della conversione in un String, quindi nessun problema ...

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

Solo due punti di buone pratiche di codifica ...

  1. Piuttosto usare String.valueOf()
  2. Alcuni standard di codifica specificano che i valori letterali di stringa dovrebbero essere l'obiettivo .equals(), anziché l'argomento, di minimizzare NullPointerExceptions.

L'unico modo per ottenere questo non accadere era usando 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);
  }
}

... in sostanza sembra che Java abbia bisogno di un po 'di tempo per riprendere fiato :)

EDIT: questo può essere del tutto casuale, ma sembra esserci una corrispondenza tra il valore che sta stampando e la tabella ASCII .

  • i= -1, il carattere visualizzato è /(valore decimale ASCII di 47)
  • i= -2, il carattere visualizzato è .(valore decimale ASCII di 46)
  • i= -3, il carattere visualizzato è -(valore decimale ASCII di 45)
  • i= -4, il carattere visualizzato è ,(valore decimale ASCII di 44)
  • i= -5, il carattere visualizzato è +(valore decimale ASCII di 43)
  • i= -6, il carattere visualizzato è *(valore decimale ASCII di 42)
  • i= -7, il carattere visualizzato è )(valore decimale ASCII di 41)
  • i= -8, il carattere visualizzato è ((valore decimale ASCII di 40)
  • i= -9, il carattere visualizzato è '(valore decimale ASCII di 39)

Ciò che è veramente interessante è che il carattere in ASCII decimale 48 è il valore 0e 48 - 1 = 47 (carattere /), ecc ...


1
Il valore numerico del carattere "/" è "-1" ??? da dove viene? ( (int)'/' == 47; (char)-1è indefinito 0xFFFFè <non è un carattere> in Unicode)
user85421-Banned

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

come si getNumericValue()collega a un determinato codice ??? e come si converte -1in '/'??? Perché non farlo '-', getNumericValue('-')è anche -1??? (A proposito molti metodi ritornano -1)
user85421-Bannato

@CarlosHeuberger, stavo correndo getNumericValue()su value( /) per ottenere il valore del personaggio. Hai ragione al 100% che il valore decimale ASCII di /dovrebbe essere 47 (era quello che mi aspettavo anche), ma getNumericValue()stava restituendo -1 a quel punto come avevo aggiunto System.out.println(Character.getNumericValue(value.toCharArray()[0]));. Riesco a vedere la confusione a cui ti riferisci e ho aggiornato il post.
Ambro-r

1

Non so perché Java stia dando un output così casuale ma il problema è nella tua concatenazione che fallisce per valori più grandi iall'interno del forciclo.

Se si sostituisce la String value = i + "";riga con String value = String.valueOf(i) ;il codice funziona come previsto.

La concatenazione che utilizza +per convertire int in stringa è nativa e potrebbe essere errata (stranamente lo stiamo trovando ora probabilmente) e causando tale problema.

Nota: ho ridotto il valore di i inside per loop a 10000 e non ho riscontrato problemi con la +concatenazione.

Questo problema deve essere segnalato agli stakeholder Java e possono esprimere la propria opinione sullo stesso.

Modifica Ho aggiornato il valore di i in for loop a 3 milioni e ho visto una nuova serie di errori come di seguito:

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)

La mia versione di Java è 8.


1
Non penso che la concatenazione di stringhe sia nativa - utilizza solo StringConcatFactory(OpenJDK 13) o StringBuilder(Java 8)
user85421-Banned

@CarlosHeuberger Possible too. Penso che sia da Java 9 se deve essere di StringConcatFactory classe. ma per quanto ne so java fino a java 8 java don; t supporto operatore di sovraccarico
Vinay Prajapati

@Vinay, ho provato anche questo e sì, funziona, ma nel momento in cui aumenti il ​​loop da 30000 a dire 3000000 inizi a riscontrare lo stesso problema.
Ambro-r

@ Ambro-r Ho provato con il valore suggerito e ho ricevuto un Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1errore. Strano.
Vinay Prajapati,

3
i + ""è compilato esattamente come new StringBuilder().append(i).append("").toString()in Java 8, e l'utilizzo che alla fine produce anche l'output
user85421-Banned
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.