我刚刚制作了这个简单的"程序":
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
为long时,在运行程序时我需要等待一段时间才能看到第一个loop: 1
.在调试器中,在暂停程序时,i
会增加:它不是0,所以它可以正常工作.
++i
作为int有什么问题?
如果继续递增整数类型,它最终会溢出,成为一个大的负值.如果你继续前进,它最终将再次变为0,并且循环将重复.
有一些方便的方法可以帮助避免意外溢出,例如Math.addExact()
,这些方法通常不会在循环中使用.
我知道它溢出来了.我只是感到困惑,它快速溢出.我发现很奇怪,每次我暂停调试器时,我都是0.
暂停正在运行的线程时,请考虑线程缓慢调用println()
遍历大量Java和本机操作系统代码的可能性,而不是在while循环测试中着陆的可能性,这只会增加本地变量.你必须有一个非常快速的触发手指来看除print语句以外的任何东西.尝试单步执行.
当事情连续发生40亿次时,这是一个很好的猜测它将在下次发生.在任何情况下,分支预测都会有所帮助,优化运行时可能会删除增量操作并完全测试,因为i
从不读取中间值.
正如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::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确实似乎检测到打印输出没有真正的"条件",并且代码相当于只打印和增加单个变量的无限循环.
这对我来说似乎是一个相当复杂的优化.我希望能够检测出这样的情况并非易事.但显然,他们设法做到了......
你的循环溢出i
.你没有break
,所以在一段时间后,i
回到0,这会打印语句和增量k
.这也解释了为什么更改int
为a long
会导致打印速度变慢:long
值溢出需要更长的时间.
首先让我们看一下逻辑上的循环.
i
会反复溢出.循环的每2 32(约40亿)次迭代将打印输出并且k将递增.
这是逻辑观点.但是,允许编译器和运行时进行优化,如果每秒钟后得到的值超过一个值,那么很明显必须进行这样的优化.即使使用现代分支预测,乱序执行等,我发现CPU不太可能在每个时钟周期内绕过一个紧密的循环(甚至我认为不太可能).事实上,在调试器中你从未看到过零以外的东西,这强化了代码被优化掉的想法.
你提到使用"long"并且你确实看到其他值时需要更长的时间.如果在未经优化的循环中使用"长"计数器,则可以预期值之间存在数十年.显然,优化正在进行,但似乎优化者在完全优化掉无意义的迭代之前就放弃了.