rules of for loop in java




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

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

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 i sendo -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 uma verificação antes da conversão em uma String , 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 literais String devem ser o destino de .equals() , em vez de argumento, minimizando assim NullPointerExceptions.

A única maneira de conseguir que isso não ocorresse 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 0 e 48 - 1 = 47 (caractere / ), etc ...


Isso pode ser reproduzido de forma confiável (ou não, dependendo do que você deseja) com a openjdk version "1.8.0_222" (usada 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:+PrintCompilation vezes suficientes 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). Na execução do buggy, o test() é compilado (ou descompilado) 5 vezes.

Além disso, executando com -XX:-TieredCompilation (que interpreta ou usa C2 ) ou com -Xbatch (que força a compilação a ser executada no encadeamento principal, em vez de paralelamente), a saída é garantida e com 30000 iterações imprime um muita coisa, então o compilador C2 parece ser o culpado. Isso é confirmado executando -XX:TieredStopAtLevel=1 , que desativa o C2 e não produz saída (a parada 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 (que é C1 , veja o link anterior).

Definitivamente, é um bug no C2 , embora eu não tenha certeza absoluta se 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 this 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 que eu tenho são dos anos 90, então vou tentar apangin como ele pode ser uma das pessoas que sabe o que está acontecendo.

É provável que já exista um relatório de bug sobre isso, já que o código foi apresentado ao OP por outra pessoa, e como todo o código C2 não está sem bugs (isso pode até estar relacionado a isso). 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 :)





jit