Подтвердить что ты не робот

Java integer ++ я не меняет значение

Я просто сделал эту простую "программу":

public static void main(String[] args) {
        int i = 1;
        int k = 0;
        while (true) {
            if(++i==0) System.out.println("loop: " + ++k);
        }
    }

После запуска этой программы я сразу получаю вывод:

(...)
loop: 881452
loop: 881453
loop: 881454
loop: 881455
loop: 881456
loop: 881457
loop: 881458
(...)

как будто i всегда будет 0.

И на самом деле, когда я отлаживаю Eclipse, при приостановке программы i всегда будет нулевым. При переходе через цикл, i будет увеличиваться, но после возобновления и приостановки отладчика i снова будет 0.

Когда я меняю i на длинные, после запуска программы мне нужно подождать довольно долго, прежде чем увидеть первый loop: 1. В отладчике после приостановки программы i делает increment: это не 0, поэтому он работает так, как должен.

Какая проблема с ++i как int?

4b9b3361

Ответ 1

Если вы продолжаете увеличивать целочисленный тип, он в конечном итоге переполняется, становясь большим отрицательным значением. Если вы продолжите движение, он снова станет 0, и цикл повторится.

Существуют удобные методы, которые помогут избежать непреднамеренных переполнений, например Math.addExact(), но они обычно не будут использоваться в цикле.


Я знаю, что он переполнен. Я просто озадачен тем, что он быстро переполняет это. И мне кажется странным, что каждый раз, когда я приостанавливаю отладчик, я равно 0.

Когда вы приостанавливаете текущий поток, рассмотрите вероятность того, что поток будет медленным вызовом println(), который пересекает огромный стек Java и собственный код ОС, а также вероятность посадки в тесте вашего цикла while, который просто увеличивает локальную переменную. У вас должен быть довольно быстрый триггерный палец, чтобы увидеть что-то другое, кроме инструкции печати. Попробуйте выполнить переход.

Когда что-то происходит 4 миллиарда раз подряд, довольно неплохо, что это произойдет в следующий раз. Прогнозирование ветки поможет здесь в любом случае, и возможно, что ваша оптимизирующая среда выполнения полностью удаляет операцию приращения и полностью тестирует, так как промежуточные значения i никогда не читаются.

Ответ 2

Как JohannesD, предложенный в комментарии, вряд ли можно считать от 0 до Integer.MAX_VALUE (и после переполнения от -Integer.MAX_VALUE до 0 снова) так быстро.

Чтобы убедиться в предположении, что JIT делает некоторую магическую оптимизацию здесь, я создал слегка измененную программу, введя некоторые методы, чтобы было легче идентифицировать части кода:

class IntOverflowTest
{
    public static void main(String[] args) {
        runLoop();
    }

    public static void runLoop()
    {
        int i = 1;
        int k = 0;
        while (true) {
            if(++i==0) doPrint(++k);
        }
    }

    public static void doPrint(int k)
    {
        System.out.println("loop: " + k);
    }

}

Байт-код, испущенный и показанный с помощью javap -c IntOverflowTest, не вызывает сюрпризов:

class IntOverflowTest {
  IntOverflowTest();
    Code:
       0: aload_0
       1: invokespecial #1                  
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #2                  
       3: return

  public static void runLoop();
    Code:
       0: iconst_1
       1: istore_0
       2: iconst_0
       3: istore_1
       4: iinc          0, 1
       7: iload_0
       8: ifne          4
      11: iinc          1, 1
      14: iload_1
      15: invokestatic  #3                  
      18: goto          4

  public static void doPrint(int);
    Code:
       0: getstatic     #4                  
       3: new           #5                  
       6: dup
       7: invokespecial #6                  
      10: ldc           #7                  
      12: invokevirtual #8                  
      15: iload_0
      16: invokevirtual #9                  
      19: invokevirtual #10                 
      22: invokevirtual #11                 
      25: return
}

Он явно увеличивает как локальные переменные (runLoop, смещения 4 и 11).

Однако при запуске кода с -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+PrintAssembly в дизассемблере Hotspot, машинный код в конечном итоге заканчивается следующим образом:

Decoding compiled method 0x00000000025c2c50:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x000000001bb40408} 'runLoop' '()V' in 'IntOverflowTest'
  #           [sp+0x20]  (sp of caller)
  0x00000000025c2da0: mov    %eax,-0x6000(%rsp)
  0x00000000025c2da7: push   %rbp
  0x00000000025c2da8: sub    $0x10,%rsp         ;*synchronization entry
                                                ; - IntOverflowTest::[email protected] (line 10)

  0x00000000025c2dac: mov    $0x1,%ebp          ;*iinc
                                                ; - IntOverflowTest::[email protected] (line 13)

  0x00000000025c2db1: mov    %ebp,%edx
  0x00000000025c2db3: callq  0x00000000024f6360  ; OopMap{off=24}
                                                ;*invokestatic doPrint
                                                ; - IntOverflowTest::[email protected] (line 13)
                                                ;   {static_call}
  0x00000000025c2db8: inc    %ebp               ;*iinc
                                                ; - IntOverflowTest::[email protected] (line 13)

  0x00000000025c2dba: jmp    0x00000000025c2db1  ;*invokestatic doPrint
                                                ; - IntOverflowTest::[email protected] (line 13)

  0x00000000025c2dbc: mov    %rax,%rdx
  0x00000000025c2dbf: add    $0x10,%rsp
  0x00000000025c2dc3: pop    %rbp
  0x00000000025c2dc4: jmpq   0x00000000025b0d20  ;   {runtime_call}
  0x00000000025c2dc9: hlt

Можно ясно видеть, что он больше не увеличивает внешнюю переменную i. Он вызывает только метод doPrint, увеличивает одну переменную (k в коде), а затем сразу же возвращается к точке перед вызовом doPrint.

Таким образом, JIT действительно обнаруживает, что для печати результата нет реального "условия", и что код эквивалентен бесконечному циклу, который только печатает и увеличивает единую переменную.

Это кажется довольно сложной оптимизацией для меня. Я ожидал бы, что это далеко не тривиально, чтобы обнаружить такой случай. Но, очевидно, им это удалось...

Ответ 3

Ваша петля переполнена i. У вас нет break, поэтому по истечении определенного периода времени i возвращается к 0, и это печатает инструкцию и увеличивает k. Это также объясняет, почему изменение int на long приводит к замедлению печати: для переполнения значения long требуется намного больше времени.

Ответ 4

Сначала рассмотрим, что этот цикл логически делает.

i будет многократно переполняться. Каждые 2 32 (около 4 миллиардов) итераций цикла вывод будет напечатан, а k будет увеличен.

Это логическое представление. Тем не менее, компиляторы и среды выполнения могут оптимизироваться, и если вы получаете больше, чем значение каждую секунду или около того, довольно ясно, что такая оптимизация должна иметь место. Даже при современном прогнозировании ветвлений, выходе из строя и т.д., Я считаю маловероятным, чтобы процессор обходил круговую петлю более одного раза за такт (и даже это я считаю маловероятным). Тот факт, что вы никогда не видите ничего, кроме нуля в отладчике, усиливает идею о том, что код оптимизирован.

Вы говорите, что это занимает больше времени, когда используется "long", и что вы видите другие значения. Если "длинный" счетчик использовался в неоптимизированном цикле, вы ожидали бы много десятилетий между значениями. Снова ясно, что оптимизация происходит, но, похоже, оптимизатор отказывается, прежде чем полностью оттолкнет бессмысленные итерации.

Ответ 5

Он не всегда равен 0, после цикла (целочисленное переполнение) он становится 0, поэтому он должен сначала стать Integer.MAX_VALUE, затем Integer.MIN_VALUE, а затем снова работать до 0. Поэтому кажется, что он всегда равен 0, но на самом деле он принимает все возможные значения целых чисел до того, как станет 0... Снова и снова.