Вихід -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
Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew

Відповіді:


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-х, тому я дозволю комусь розумнішим за мене звідси.

Ймовірно, що про це вже є звіт про помилку, оскільки код був представлений ОП кимось іншим, і як і весь код С2, не має помилок . Я сподіваюся, що цей аналіз був таким же інформативним для інших, як і для мене.

Як в коментарях вказав поважний апангін, це нещодавня помилка . Дуже зобов’язаний усім зацікавленим та корисним людям :)


Я також думаю, що це C2- подивився згенерований код асемблера (і спробував його зрозуміти) за допомогою JitWatch - C1згенерований код все ще нагадує байт-код, C2абсолютно інший (я навіть не міг знайти ініціалізацію iз 8)
user85421-заборонено

Ваша відповідь дуже хороша, я спробував, відключити c2, результат правильний. Однак загалом більшість цих параметрів за замовчуванням у проекті, хоча власне проект не матиме наведеного вище коду, але, швидше за все, він має аналогічний код, якщо проект використовує аналогічний код, це справді страшно
okali

1
@Eugene, це було досить хитро, я був впевнений, що це буде щось на зразок помилки компілятора затемнення чи подібного ... і я спершу не міг його відтворити ..
Kayaman

1
@Kayaman погодився. Аналіз, який ви зробили, дуже хороший, апангін повинен бути більше, щоб це пояснити та виправити. Який казковий ранок у поїзді!
Євген

7
Я помітив цю тему лише випадково. Щоб переконатися, що я бачу запитання, використовуйте @mentions або додайте тег #jvm. Хороший аналіз, BTW. Це справді помилка компілятора С2, виправлена ​​лише кілька днів тому - JDK-8231988 .
apangin

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. Деякі стандарти кодування визначають, що рядкові літерали повинні бути ціллю .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);
  }
}

... по суті, це просто схоже на те, що Яві потрібно трохи часу, щоб перевести дух :)

EDIT: Це може бути зовсім збігом, але, здається, існує деяка відповідність між значенням, яке роздруковується, та таблицею 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 (символ /) тощо.


1
він числове значення символу "/" дорівнює "-1" ??? звідки це походить? ( (int)'/' == 47; (char)-1не визначено, 0xFFFFце <не персонаж> в Unicode)
user85421-Заборонено

1
char c = '/'; int a = Character.getNumericValue (c); System.out.println (a);
Амбро-р

як getNumericValue()ставиться до даного коду ??? і як це перетворюється -1в '/'??? Чому б і ні '-', getNumericValue('-')це теж -1??? (BTW повертається багато методів -1)
user85421-Заборонено

@CarlosHeuberger, я працював getNumericValue()на value( /), щоб отримати значення символу. Ви на 100% вірні, що десяткове значення ASCII /має становити 47 (це було те, чого я також очікував), але getNumericValue()повертав -1 у той момент, як я додав System.out.println(Character.getNumericValue(value.toCharArray()[0]));. Я бачу плутанину, на яку ви посилаєтесь, і оновив публікацію.
Ambro-r

1

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

Якщо ви заміните String value = i + "";рядок зі String value = String.valueOf(i) ;своїм кодом, працює, як очікувалося.

Зв'язування, яке використовує +для перетворення int у рядок, є рідним і може бути помилковим (як не дивно, ми зараз це знаходимо) і спричиняє подібні проблеми.

Примітка. Я зменшив значення i всередині циклу до 10000, і не зіткнувся з проблемою з +конкатенацією.

Про це питання потрібно повідомити зацікавленим сторонам Java, і вони можуть висловити свою думку з цього питання.

Редагувати Я оновив значення i in для циклу до 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-Заборонено

@CarlosHeuberger Можливо також. Я думаю, що це від java 9, якщо це має бути StringConcatFactory клас. але наскільки я знаю java до java 8 java don; t оператор підтримки перевантаження
Vinay Prajapati

@Vinay, також спробував це, і так, це працює, але в момент, коли ти збільшиш цикл з 30000, щоб сказати, 3000000, у тебе починають виникати ті ж проблеми.
Ambro-r

@ Ambro-r Я спробував із запропонованим вами значенням, і я отримую Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1помилку. Дивно.
Vinay Prajapati

3
i + ""компілюється точно так само, як new StringBuilder().append(i).append("").toString()у Java 8, і за допомогою цього в кінцевому підсумку виходить вихід
user85421-Заборонено
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.