我刚刚制作了这个简单的"程序":
$("#LineaDelTiempoDiv")
运行此程序后,我立即得到输出:
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
(...)
总是0。
事实上,当我在Eclipse中调试时,在暂停程序时,i
将始终为零。当单步执行循环时,i
会递增,但在恢复和挂起调试器后,i
再次为0。
当我将i
更改为long时,在运行程序时,我需要等待一段时间才能看到第一个i
。在调试器中,暂停程序后,loop: 1
会增加:它不是0,所以它可以正常工作。
i
作为int的问题是什么?
答案 0 :(得分:49)
如果继续递增整数类型,它最终会溢出,成为一个较大的负值。如果你继续前进,它最终将再次变为0,并且循环将重复。
有一些方便的方法可以帮助避免意外溢出,例如Math.addExact()
,但这些方法通常不会在循环中使用。
我知道它溢出来了。我很困惑,它快速溢出。我发现很奇怪,每次我暂停调试器时,我都是0。
暂停正在运行的线程时,请考虑线程缓慢调用遍历大量Java和本机操作系统代码的println()
的可能性,而不是在您的测试中着陆的可能性。循环,它只是递增局部变量。您必须有一个非常快速的触发手指才能看到除print语句之外的任何内容。请尝试单步执行。
如果连续发生了40亿次,那么下次就会发生这种情况。在任何情况下,分支预测都会有所帮助,优化运行时可能会删除增量操作并完全测试,因为i
的介入值永远不会被读取。
答案 1 :(得分:22)
作为JohannesD suggested in a comment,几乎不可能从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)。
但是,在Hotspot反汇编程序中运行带-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+PrintAssembly
的代码时,机器代码最终会达到以下结果:
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::runLoop@-1 (line 10)
0x00000000025c2dac: mov $0x1,%ebp ;*iinc
; - IntOverflowTest::runLoop@11 (line 13)
0x00000000025c2db1: mov %ebp,%edx
0x00000000025c2db3: callq 0x00000000024f6360 ; OopMap{off=24}
;*invokestatic doPrint
; - IntOverflowTest::runLoop@15 (line 13)
; {static_call}
0x00000000025c2db8: inc %ebp ;*iinc
; - IntOverflowTest::runLoop@11 (line 13)
0x00000000025c2dba: jmp 0x00000000025c2db1 ;*invokestatic doPrint
; - IntOverflowTest::runLoop@15 (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确实似乎发现没有真正的"条件"涉及打印输出,并且代码相当于一个无限循环,只打印并递增单个变量。
这对我来说似乎是一个非常复杂的优化。我希望能够检测到这样的案例远非微不足道。但显然,他们设法做到了......
答案 2 :(得分:11)
你的循环溢出i
。您没有break
,因此在一段时间后,i
回绕到0,这将打印语句并递增k
。这也解释了为什么将int
更改为long
会导致打印速度变慢:long
值溢出需要更长的时间。
答案 3 :(得分:10)
首先让我们看一下逻辑上的循环。
i
会反复溢出。每循环2次 32 (约40亿次),输出将被打印,k将递增。
这是逻辑观点。但是,允许编译器和运行时进行优化,如果每秒钟后得到的值超过一个值,那么很明显必须进行这样的优化。即使使用现代分支预测,乱序执行等,我发现CPU不太可能在每个时钟周期内绕过一个紧密的循环(甚至我认为不太可能)。在调试器中你从未看到过零以外的事实强化了代码被优化的想法。
您提到使用“long”时需要更长的时间并且您确实看到了其他值。如果在未经优化的循环中使用“长”计数器,则可以预期值之间存在数十年。再次明显优化正在进行,但似乎优化者在完全优化掉无意义的迭代之前就放弃了。
答案 4 :(得分:4)
它并不总是0,它在循环结束后变为0(整数溢出),因此它首先变为Integer.MAX_VALUE,然后是Integer.MIN_VALUE,然后再次向上运行0。这就是为什么它似乎总是为0,但事实上它在变为0之前需要所有可能的整数值......一遍又一遍。