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