A saída -1 se torna uma barra no loop

54

Surpreendentemente, o seguinte código gera:

/
-1

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

}

Tentei várias vezes determinar quantas vezes isso ocorreria, mas, infelizmente, era incerto, e descobri que a saída de -2 às vezes se transformava em um período. Além disso, também tentei remover o loop while e a saída -1 sem problemas. Quem pode me dizer o porquê?


Informações da versão do JDK:

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1
okali
fonte
2
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Samuel Liew

Respostas:

36

Isso pode ser reproduzido de maneira confiável (ou não, dependendo do que você deseja) com openjdk version "1.8.0_222"(usado em minha análise), OpenJDK 12.0.1(de acordo com Oleksandr Pyrohov) e OpenJDK 13 (de acordo com Carlos Heuberger).

Corri o código com -XX:+PrintCompilationtempo suficiente para obter os dois comportamentos e aqui estão as diferenças.

Implementação de buggy (exibe saída):

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

Execução correta (sem exibição):

 --- 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 uma diferença significativa. Com a execução correta, compilamos test()duas vezes. Uma vez no início e mais uma vez depois (presumivelmente porque o JIT percebe o quão quente é o método). No buggy, a execução test()é compilada (ou descompilada) 5 vezes.

Além disso, executando com -XX:-TieredCompilation(que interpreta ou usa C2) ou com -Xbatch(que força a compilação a executar no encadeamento principal, em vez de paralelamente), a saída é garantida e, com 30000 iterações, imprime muitas coisas, portanto o C2compilador parece ser o culpado. Isso é confirmado com a execução de -XX:TieredStopAtLevel=1, que desativa C2e não produz saída (parar no nível 4 mostra o bug novamente).

Na execução correta, o método é compilado primeiro com a compilação do Nível 3 e depois com o Nível 4.

Na execução do buggy, as compilações anteriores são descartadas ( made non entrant) e são compiladas novamente no nível 3 (ou seja C1, consulte o link anterior).

Definitivamente, é um bug C2, embora eu não tenha certeza absoluta de que o fato de voltar à compilação do nível 3 a afeta (e por que está voltando ao nível 3, ainda há muitas incertezas).

Você pode gerar o código de montagem com a seguinte linha para ir ainda mais fundo na toca do coelho (veja também isso para ativar a impressão de montagem).

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

Neste ponto, estou começando a ficar sem habilidades, o comportamento do buggy começa a ser exibido quando as versões compiladas anteriores são descartadas, mas que poucas habilidades de montagem eu tenho desde os anos 90, então deixarei alguém mais esperto do que eu usá-lo daqui.

É provável que já exista um relatório de bug sobre isso, uma vez que o código foi apresentado ao OP por outra pessoa e, como todo o código C2, não há erros . Espero que essa análise tenha sido tão informativa para os outros quanto para mim.

Como o venerável apangin apontou nos comentários, este é um bug recente . Muito obrigado a todas as pessoas interessadas e prestativas :)

Kayaman
fonte
Eu também acho que é C2- olhei o código do assembler gerado (e tentei entendê-lo) usando o C1código gerado pelo JitWatch - ainda se assemelha ao bytecode, C2é totalmente diferente (eu nem consegui encontrar a inicialização icom 8)
user85421-Banned
sua resposta é muito boa, tentei desabilitar c2, o resultado está correto. No entanto, em geral, a maioria desses parâmetros é padrão no projeto, embora o projeto real não tenha o código acima, mas é provável que tenha um código semelhante, se o projeto usar código semelhante, é realmente terrível
okali
11
@Eugene este tem sido um bastante complicado, eu tinha certeza que ia ser algo como bug do compilador eclipse ou similar ... e eu não poderia reproduzi-lo em primeiro lugar quer ..
Kayaman
11
@Kayaman concordou. A análise que você fez é muito boa; deve ser mais do que suficiente para o apangin explicar e corrigir isso. Que manhã fabulosa no trem!
31419 Eugene
7
Notei esse tópico apenas acidentalmente. Para garantir que eu veja a pergunta, use @ menções ou adicione uma tag #jvm. Boa análise, BTW. Este é realmente um bug do compilador C2, corrigido apenas alguns dias atrás - JDK-8231988 .
Apangin 31/10/19
4

Isso é honestamente bem estranho, pois esse código tecnicamente nunca deve ser exibido porque ...

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

... sempre deve resultar em iser -1(8 - 3 = 5; 5 - 3 = 2; 2 - 3 = -1). O que é ainda mais estranho é que ele nunca sai no modo de depuração do meu IDE.

Curiosamente, no momento em que adiciono um cheque antes da conversão para a String, então não há 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);
  }
}

Apenas dois pontos de boas práticas de codificação ...

  1. Em vez disso, use String.valueOf()
  2. Alguns padrões de codificação especificam que os literais String devem ser o alvo .equals(), e não o argumento, minimizando assim NullPointerExceptions.

A única maneira de conseguir que isso não acontecesse 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);
  }
}

... basicamente parece que o Java precisa de um pouco de tempo para recuperar o fôlego :)

EDIT: Isso pode ser completamente coincidência, mas parece haver alguma correspondência entre o valor que está sendo impresso e a tabela ASCII .

  • i= -1, o caractere exibido é /(valor decimal ASCII de 47)
  • i= -2, o caractere exibido é .(valor decimal ASCII de 46)
  • i= -3, o caractere exibido é -(valor decimal ASCII de 45)
  • i= -4, o caractere exibido é ,(valor decimal ASCII de 44)
  • i= -5, o caractere exibido é +(valor decimal ASCII de 43)
  • i= -6, o caractere exibido é *(valor decimal ASCII de 42)
  • i= -7, o caractere exibido é )(valor decimal ASCII de 41)
  • i= -8, o caractere exibido é ((valor decimal ASCII de 40)
  • i= -9, o caractere exibido é '(valor decimal ASCII de 39)

O que é realmente interessante é que o caractere no decimal ASCII 48 é o valor 0e 48 - 1 = 47 (caractere /), etc ...

Ambro-r
fonte
11
o valor numérico do caractere "/" é "-1" ??? De onde isto vem? ( (int)'/' == 47; (char)-1é indefinido 0xFFFF<não é um caractere> em Unicode) #
user85421-banido
11
char c = '/'; int a = Character.getNumericValue (c); System.out.println (a);
quer
como se getNumericValue()relaciona com o código fornecido ?? e como ele se converte -1em '/'??? Por que não '-', getNumericValue('-')também é -1??? (BTW muitos métodos retornam -1)
user85421-Banned
@CarlosHeuberger, eu estava rodando getNumericValue()em value( /) para obter o valor do personagem. Você está 100% correto que o valor decimal ASCII /deve ser 47 (era o que eu também esperava), mas getNumericValue()estava retornando -1 nesse ponto, como eu havia adicionado System.out.println(Character.getNumericValue(value.toCharArray()[0]));. Eu posso ver a confusão a que você está se referindo e atualizou a postagem.
quer
1

Não sei por que o Java está fornecendo uma saída aleatória, mas o problema está na sua concatenação que falha em valores maiores identro do forloop.

Se você substituir a String value = i + "";linha pelo String value = String.valueOf(i) ;seu código, funcionará conforme o esperado.

A concatenação usada +para converter o int em string é nativa e pode ser incorreta (estranhamente, estamos fundando agora, provavelmente) e causando esse problema.

Nota: Reduzi o valor de i inside for loop para 10000 e não tive problemas com +concatenação.

Esse problema deve ser relatado aos interessados ​​em Java e eles podem dar sua opinião sobre o mesmo.

Editar Atualizei o valor de i in for loop para 3 milhões e vi um novo conjunto de erros como abaixo:

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)

Minha versão do Java é 8.

Vinay Prajapati
fonte
11
Eu não acho que concatenação é nativo - ele só usa StringConcatFactory(OpenJDK 13) ou StringBuilder(Java 8)
Banido-user85421
@CarlosHeuberger Possível também. Eu acho que é do java 9 se tiver que ser StringConcatFactory classe. mas, tanto quanto eu sei java até java 8 java don; operador t apoio sobrecarga
Vinay Prajapati
@ Vinay, tentei isso também e sim, funciona, mas no momento em que você aumenta o loop de 30000 para dizer 3000000, você começa a ter o mesmo problema.
Ambro-r
@ Ambro-r Eu tentei com o seu valor sugerido e recebo Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1erro. Estranho.
Vinay Prajapati
3
i + ""é compilado exatamente como new StringBuilder().append(i).append("").toString()no Java 8, e usá-lo também produz a saída
user85421-Banned