La salida -1 se convierte en una barra en el bucle

54

Sorprendentemente, el siguiente código sale:

/
-1

El código:

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

}

Intenté muchas veces determinar cuántas veces ocurriría esto, pero, desafortunadamente, fue en última instancia incierto, y descubrí que la producción de -2 a veces se convertía en un período. Además, también intenté eliminar el bucle while y la salida -1 sin ningún problema. ¿Quién me puede decir por qué?


Información de la versión de JDK:

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1
okali
fuente
2
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Samuel Liew

Respuestas:

36

Esto se puede reproducir de manera confiable (o no, según lo que desee) con openjdk version "1.8.0_222"(utilizado en mi análisis), OpenJDK 12.0.1(según Oleksandr Pyrohov) y OpenJDK 13 (según Carlos Heuberger).

Ejecuté el código con -XX:+PrintCompilationsuficientes tiempos para obtener ambos comportamientos y aquí están las diferencias.

Implementación con errores (muestra la salida):

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

Ejecución correcta (sin visualización):

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

Podemos notar una diferencia significativa. Con la ejecución correcta compilamos test()dos veces. Una vez al principio, y una vez más después (presumiblemente porque el JIT nota cuán caliente es el método). En el buggy la ejecución test()se compila (o descompila) 5 veces.

Además, al ejecutarse con -XX:-TieredCompilation(que interpreta o utiliza C2) o con -Xbatch(que obliga a la compilación a ejecutarse en el hilo principal, en lugar de en paralelo), se garantiza la salida y con 30000 iteraciones imprime muchas cosas, por lo que el C2compilador parece ser el culpable Esto se confirma al ejecutar with -XX:TieredStopAtLevel=1, que deshabilita C2y no produce salida (detenerse en el nivel 4 muestra el error nuevamente).

En la ejecución correcta, el método se compila primero con la compilación de Nivel 3 y luego con el Nivel 4.

En la ejecución con errores, se descartan las compilaciones anteriores (made non entrant ) y se vuelve a compilar en el Nivel 3 (es decir C1, ver enlace anterior).

Entonces definitivamente es un error en C2 , aunque no estoy absolutamente seguro de si el hecho de que esté volviendo a la compilación del Nivel 3 lo afecta (y por qué está volviendo al nivel 3, todavía hay muchas incertidumbres).

Puede generar el código de ensamblaje con la siguiente línea para profundizar aún más en el agujero del conejo (también vea esto para habilitar la impresión de ensamblaje).

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

En este punto, estoy empezando a quedarme sin habilidades, el comportamiento con errores comienza a exhibirse cuando se descartan las versiones compiladas anteriores, pero las pocas habilidades de ensamblaje que tengo son de los 90, así que dejaré que alguien más inteligente que yo lo tome de aquí.

Es probable que ya haya un informe de error sobre esto, ya que el código fue presentado al OP por otra persona, y como todo el código C2 no está exento de errores . Espero que este análisis haya sido tan informativo para otros como lo ha sido para mí.

Como el venerable apangin señaló en los comentarios, este es un error reciente . Muy agradecido con todas las personas interesadas y serviciales :)

Kayaman
fuente
También creo que es C2: he mirado el código de ensamblador generado (y he tratado de entenderlo) usando JitWatch: el C1código generado todavía se parece a bytecode, C2es totalmente diferente (ni siquiera pude encontrar la inicialización de icon 8)
user85421-Banned
su respuesta es muy buena, lo intenté, deshabilite c2, el resultado es correcto. Sin embargo, en general, la mayoría de estos parámetros son predeterminados en el proyecto, aunque el proyecto real no tendrá el código anterior, pero es probable que tenga un código similar, si el proyecto usa un código similar, es realmente terrible
okali
1
@Eugene, esto ha sido bastante complicado, estaba seguro de que sería algo así como un error del compilador de eclipse o similar ... y tampoco pude reproducirlo al principio ..
Kayaman
1
@Kayaman estuvo de acuerdo. El análisis que hizo es muy bueno, debería ser más que suficiente para que apangin lo explique y arregle. ¡Qué mañana fabulosa en el tren!
Eugene
77
Noté este tema solo accidentalmente. Para asegurarse de que veo la pregunta, use @menciones o agregue una etiqueta #jvm. Buen análisis, por cierto. De hecho, este es un error del compilador C2, solucionado hace solo unos días: JDK-8231988 .
Apangin
4

Esto es sinceramente bastante extraño, ya que ese código técnicamente nunca debería salir porque ...

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

... siempre debe resultar en iser -1(8 - 3 = 5; 5 - 3 = 2; 2 - 3 = -1). Lo que es aún más extraño es que nunca sale en el modo de depuración de mi IDE.

Curiosamente, en el momento en que agrego un cheque antes de la conversión a a String, entonces no hay 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 dos puntos de buena práctica de codificación ...

  1. En lugar de usar String.valueOf()
  2. Algunos estándares de codificación especifican que los literales de cadena deben ser el objetivo de .equals(), en lugar de argumento, minimizando así NullPointerExceptions.

La única forma en que conseguí que esto no ocurriera fue 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);
  }
}

... esencialmente parece que Java necesita un poco de tiempo para recuperar el aliento :)

EDITAR: Esto puede ser una coincidencia completa, pero parece haber cierta correspondencia entre el valor que se está imprimiendo y la tabla ASCII .

  • i= -1, el carácter mostrado es /(valor decimal ASCII de 47)
  • i= -2, el carácter mostrado es .(valor decimal ASCII de 46)
  • i= -3, el carácter mostrado es -(valor decimal ASCII de 45)
  • i= -4, el carácter mostrado es ,(valor decimal ASCII de 44)
  • i= -5, el carácter que se muestra es +(valor decimal ASCII de 43)
  • i= -6, el carácter que se muestra es *(valor decimal ASCII de 42)
  • i= -7, el carácter mostrado es )(valor decimal ASCII de 41)
  • i= -8, el carácter que se muestra es( (valor decimal ASCII de 40)
  • i= -9, el carácter mostrado es '(valor decimal ASCII de 39)

Lo que es realmente interesante es que el carácter en ASCII decimal 48 es el valor 0y 48 - 1 = 47 (carácter /), etc.

Ambro-r
fuente
1
El valor numérico del carácter "/" es "-1" ??? ¿De donde viene esto? ( (int)'/' == 47; (char)-1no está definido 0xFFFFes <no es un personaje> en Unicode)
usuario85421-Prohibido el
1
char c = '/'; int a = Character.getNumericValue (c); System.out.println (a);
Ambro-r
¿Cómo se getNumericValue()relaciona con el código dado? y cómo convertir -1a '/'??? ¿Por qué no '-', getNumericValue('-')también es -1??? (Por cierto, muchos métodos regresan -1)
user85421-Prohibido el
@CarlosHeuberger, estaba ejecutando getNumericValue()en value( /) para obtener el valor del personaje. Está 100% correcto de que el valor decimal ASCII de /debería ser 47 (era lo que también esperaba), pero getNumericValue()estaba devolviendo -1 en ese punto como había agregado System.out.println(Character.getNumericValue(value.toCharArray()[0]));. Puedo ver la confusión a la que te refieres y he actualizado la publicación.
Ambro-r
1

No sé por qué Java está dando una salida tan aleatoria, pero el problema está en su concatenación que falla para valores más grandes identro del forbucle.

Si reemplaza la String value = i + "";línea con String value = String.valueOf(i) ;su código funciona como se esperaba.

La concatenación que se usa +para convertir el int en una cadena es nativa y puede tener errores (curiosamente, probablemente la estamos encontrando ahora) y causar ese problema.

Nota: reduje el valor de i inside for loop a 10000 y no tuve problemas con la +concatenación.

Este problema se debe informar a las partes interesadas de Java y pueden dar su opinión sobre el mismo.

Editar Actualicé el valor de i in for loop a 3 millones y vi un nuevo conjunto de errores como se muestra a continuación:

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)

Mi versión de Java es la 8.

Vinay Prajapati
fuente
1
No creo que la concatenación de cadenas sea nativa, solo usa StringConcatFactory(OpenJDK 13) o StringBuilder(Java 8)
usuario85421-Prohibido el
@CarlosHeuberger Posible también. Creo que es de Java 9 si tiene que ser de StringConcatFactory clase. pero hasta donde yo sé java hasta java 8 java don; t soporta la sobrecarga del operador
Vinay Prajapati
@Vinay, probé esto también y sí, funciona, pero en el momento en que aumentas el ciclo de 30000 a 3000000, comienzas a tener el mismo problema.
Ambro-r
@ Ambro-r Intenté con su valor sugerido y recibo un Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1error. Extraño.
Vinay Prajapati
3
i + ""se compila exactamente como new StringBuilder().append(i).append("").toString()en Java 8, y su uso eventualmente también produce la salida
user85421-Banned