Выход -1 становится косой чертой в цикле

54

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

/
-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
Окали
источник
2
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Самуэль Лью

Ответы:

36

Это может быть надежно воспроизведено (или не воспроизведено, в зависимости от того, что вы хотите) с 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-му уровню, так много неопределенностей все еще).

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

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

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

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

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

Kayaman
источник
Я также думаю, что C2- посмотрел на сгенерированный ассемблерный код (и попытался понять его) с помощью JitWatch - C1сгенерированный код все еще напоминает байт-код, C2он совершенно другой (я даже не смог найти инициализацию iс 8)
user85421-Banned
ваш ответ очень хороший, я пробовал, отключите с2, результат верный. Однако, как правило, большинство из этих параметров являются параметрами по умолчанию в проекте, хотя сам проект не будет иметь вышеуказанного кода, но он, вероятно, будет иметь аналогичный код, если проект использует аналогичный код, это действительно ужасно
окей
1
@ Евгений, это было довольно сложно, я был уверен, что это будет что-то вроде ошибки компилятора затмения или чего-то подобного ... и я не мог сначала воспроизвести это тоже ..
Kayaman
1
@ Каяман согласился. Анализ, который вы сделали, очень хорош, его должно быть достаточно, чтобы апангин объяснил и исправил это. Какое сказочное утро в поезде!
Евгений,
7
Я заметил эту тему только случайно. Чтобы убедиться, что я вижу вопрос, используйте @mentions или добавьте тег #jvm. Хороший анализ, кстати. Это действительно ошибка компилятора C2, исправленная всего несколько дней назад - JDK-8231988 .
Апангин
4

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

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

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

Интересно, что в тот момент, когда я добавляю чек перед преобразованием в a 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. Некоторые стандарты кодирования указывают, что литералы String должны быть целью .equals(), а не аргументом, минимизируя таким образом исключения NullPointerException.

Единственный способ, которым я получил это, чтобы не произошло, было с помощью 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 .

  • 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 (символ /) и т. Д.

Ambro-р
источник
1
числовое значение символа "/" равно "-1" ??? откуда это взялось? ( (int)'/' == 47; (char)-1Неопределен 0xFFFFесть <не символ> в Unicode)
user85421-Banned
1
char c = '/'; int a = Character.getNumericValue (c); System.out.println (а);
Амбро-р
как getNumericValue()относится к данному коду ??? и как это конвертировать -1в '/'??? Почему бы и нет '-', getNumericValue('-')тоже -1??? (Кстати, многие методы возвращаются -1)
user85421-Banned
@CarlosHeuberger, я бегу getNumericValue()на value( /) , чтобы получить значение символа. Вы на 100% правы, что десятичное значение ASCII /должно быть 47 (это было то, что я также ожидал), но getNumericValue()возвращало -1 в тот момент, как я добавил System.out.println(Character.getNumericValue(value.toCharArray()[0]));. Я вижу путаницу, на которую вы ссылаетесь, и обновил пост.
Амбро-р
1

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

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

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

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

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

Изменить Я обновил значение i в для цикла до 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.

Виная Праджапати
источник
1
Я не думаю, что конкатенация строк является родной - она ​​просто использует StringConcatFactory(OpenJDK 13) или StringBuilder(Java 8)
user85421-Banned
@CarlosHeuberger Возможно тоже. Я думаю, что это от java 9, если это должен быть StringConcatFactory класс. но насколько я знаю, ява до ява 8 ява не поддерживает перегрузку оператора
Vinay
@Vinay, попробовал это тоже, и да, это работает, но как только вы увеличите цикл с 30000 до 3000000, у вас возникнет та же проблема.
Амбро-р
@ Ambro-r Я попытался с вашим предложенным значением, и я получаю Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1ошибку. Странный.
Винай Праджапати
3
i + ""компилируется точно так же, как new StringBuilder().append(i).append("").toString()в Java 8, и использование этого в конечном итоге также приводит к выводу
user85421-Banned