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?
Ответы
Ответ 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... Снова и снова.