Выход -1 становится косой чертой в цикле - PullRequest
46 голосов
/ 31 октября 2019

Удивительно, но следующий код выводит:

/
-1

Код:

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

}

Я много раз пытался определить, сколько раз это произойдет, но, к сожалению, это былов конечном счете, неопределенный, и я обнаружил, что результат -2 иногда превращается в период. Кроме того, я также попытался удалить цикл while и вывести -1 без проблем. Кто может сказать мне, почему?


Информация о версии JDK:

HopSpot 64-Bit 1.8.0.171
IDEA 2019.1.1

Ответы [ 3 ]

31 голосов
/ 31 октября 2019

Это может быть надежно воспроизведено (или не воспроизведено, в зависимости от того, что вы хотите) с помощью openjdk version "1.8.0_222" (используется в моем анализе), OpenJDK 12.0.1 (согласно Александру Пирогову) и OpenJDK 13 (согласно Карлосу Хойбергеру).

Я запускал код с -XX:+PrintCompilation достаточным количеством раз, чтобы получить оба поведения, и вот различия.

Глючная реализация (отображает вывод):

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

Правильный запуск(без дисплея):

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

Мы можем заметить одно существенное отличие. При правильном исполнении мы компилируем test() дважды. Один раз в начале и еще раз потом (предположительно потому, что JIT замечает, насколько горячий метод). В ошибочном исполнении test() компилируется (или декомпилируется) 5 раз.

Кроме того, работает с -XX:-TieredCompilation (который либо интерпретирует, либо использует C2) или с -Xbatch (что заставляет компиляцию запускаться в основном потоке, а не параллельно), вывод равен гарантировано и с 30000 итерациями выводит много материала, поэтому C2Компилятор кажется виновником. Это подтверждается запуском с -XX:TieredStopAtLevel=1, который отключает C2 и не производит вывод (остановка на уровне 4 снова показывает ошибку).

При правильном выполнении метод сначала компилируется с помощью компиляция уровня 3 , затем - с уровнем 4.

При выполнении с ошибками предыдущие компиляции отбрасываются (made non entrant) и снова компилируются на уровне 3 (то есть C1,см. предыдущую ссылку).

Так что это определенно ошибка в C2, хотя я не совсем уверен, влияет ли на нее факт, что он возвращается к компиляции 3-го уровня (и почему он возвращается куровень 3, еще много неопределенностей).

Вы можете сгенерировать код сборки с помощью следующей строки, чтобы еще глубже проникнуть в кроличью нору (см. также this , чтобы включить печать сборки).

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

В этот момент у меня заканчиваются навыки, глючное поведение начинает проявляться, когда предыдущие скомпилированные версии отбрасываются, но какая маленькая сборка лыжиlls у меня из 90-х, так что я позволю кому-нибудь умнее меня взять его отсюда.

Вполне вероятно, что об этом уже есть сообщение об ошибке, так как код был предоставлен ОП кому-тоиначе, и как весь код C2 не без ошибок . Я надеюсь, что этот анализ был таким же информативным для других, как и для меня.

Как отметил почтенный апангин в комментариях, это недавняя ошибка . Большое спасибо всем заинтересованным и полезным людям:)

3 голосов
/ 31 октября 2019

Это, честно говоря, довольно странно, поскольку этот код технически никогда не должен выводиться, потому что ...

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

... всегда должен приводить к тому, что i будет -1 (8 - 3 = 5;5 - 3 = 2; 2 - 3 = -1). Что еще более странно, так это то, что он никогда не выводится в режиме отладки моей IDE.

Интересно, что в тот момент, когда я добавляю проверку перед преобразованием в String, тогда нет проблем ...

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

Всего две точки хорошей практики кодирования ...

  1. Скорее используйте String.valueOf()
  2. Некоторые стандарты кодирования указывают, что строковые литералы должны быть целью .equals()вместо аргумента, минимизируя таким образом NullPointerExceptions.

Единственный способ, которым я этого не получил, - это использование 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);
  }
}

... по сути это выглядит такJava нужно немного времени, чтобы перевести дыхание :)

РЕДАКТИРОВАТЬ: Это может быть совершенно случайно, но, кажется, есть некоторое соответствие между значением, которое выводится на печать и ASCII Table .

  • i = -1, отображаемый символ / (десятичное значение ASCII 47)
  • i = -2, отображаемый символ .(Десятичное значение ASCII 46)
  • i = -3, отображаемый символ - (десятичное значение ASCII 45)
  • i = -4, отображаемый символ, (десятичное значение ASCII 44)
  • i = -5, отображаемый символ + (десятичное значение ASCII 43)
  • i = -6, отображаемый символ * (десятичное значение ASCII 42)
  • i = -7, отображаемый символ ) (десятичное значение ASCII 41)
  • i = -8, отображаемый символ ( (десятичное значение ASCII 40)
  • i = -9, отображаемый символ ' (десятичное значение ASCII 39)

Что действительно интересно, так это то, что десятичный символ ASCII 48 - это значение 0, а 48 - 1 = 47 (символ /) и т. Д. *

0 голосов
/ 31 октября 2019

Не знаю, почему Java выдает такой случайный вывод, но проблема в вашей конкатенации, которая не работает при больших значениях i внутри цикла for.

Если вы замените строку String value = i + ""; на String value = String.valueOf(i) ;, ваш код будет работать как положено.

Конкатенация с использованием + для преобразования целого числа в строку является встроенной и может содержать ошибки (странно, мы, вероятно, сейчас ее находим) и вызывает такую ​​проблему.

Примечание: я уменьшил значениея внутри для цикла до 10000, и у меня не возникало проблем с конкатенацией +.

Об этой проблеме необходимо сообщить заинтересованным сторонам Java, и они могут высказать свое мнение по этому поводу.

Редактировать Я обновил значение цикла i в for до 3 миллионов и увидел новый набор ошибок, как показано ниже:

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)

Моя версия Java - 8.

...