为什么40亿次迭代的Java循环只需2 ms?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么40亿次迭代的Java循环只需2 ms?相关的知识,希望对你有一定的参考价值。
我在配备2.7 GHz Intel Core i7的笔记本电脑上运行以下Java代码。我打算让它测量完成2 ^ 32次迭代循环所需的时间,我预计大约需要1.48秒(4 / 2.7 = 1.48)。
但实际上它只需要2毫秒,而不是1.48秒。我想知道这是否是下面任何JVM优化的结果?
public static void main(String[] args)
{
long start = System.nanoTime();
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++){
}
long finish = System.nanoTime();
long d = (finish - start) / 1000000;
System.out.println("Used " + d);
}
这里有两种可能性之一:
- 编译器意识到循环是冗余的,什么也不做,所以它优化了它。
- JIT(即时编译器)意识到循环是冗余的,什么都不做,所以它优化了它。
现代编译器非常聪明;他们可以看到代码何时无用。尝试将一个空循环放入GodBolt并查看输出,然后打开-O2
优化,你会看到输出是沿着
main():
xor eax, eax
ret
我想澄清一下,在Java中,大多数优化都是由JIT完成的。在其他一些语言(如C / C ++)中,大多数优化都是由第一个编译器完成的。
看起来它被JIT编译器优化了。当我关闭它(-Djava.compiler=NONE
)时,代码运行得慢得多:
$ javac MyClass.java
$ java MyClass
Used 4
$ java -Djava.compiler=NONE MyClass
Used 40409
我将OP的代码放在class MyClass
中。
我只是陈述了显而易见的事实 - 这是一个JVM优化发生,循环将完全删除。这是一个小测试,显示JIT
在启用/启用C1 Compiler
和禁用时的巨大差异。
免责声明:不要写这样的测试 - 这只是为了证明实际循环“删除”发生在C2 Compiler
:
@Benchmark
@Fork(1)
public void full() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
++result;
}
}
@Benchmark
@Fork(1)
public void minusOne() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
++result;
}
}
@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-XX:TieredStopAtLevel=1" })
public void withoutC2() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
++result;
}
}
@Benchmark
@Fork(value = 1, jvmArgsAppend = { "-Xint" })
public void withoutAll() {
long result = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE - 1; i++) {
++result;
}
}
结果显示,取决于启用JIT
的哪一部分,方法变得更快(速度快得多,看起来它正在“无所事事” - 循环移除,这似乎发生在C2 Compiler
- 这是最高级别):
Benchmark Mode Cnt Score Error Units
Loop.full avgt 2 ≈ 10⁻⁷ ms/op
Loop.minusOne avgt 2 ≈ 10⁻⁶ ms/op
Loop.withoutAll avgt 2 51782.751 ms/op
Loop.withoutC2 avgt 2 1699.137 ms/op
正如已经指出的那样,JIT(即时)编译器可以优化空循环以便去除不必要的迭代。但是怎么样?
实际上,有两个JIT编译器:C1和C2。首先,使用C1编译代码。 C1收集统计信息并帮助JVM发现在100%的情况下我们的空循环不会改变任何东西而且是无用的。在这种情况下,C2进入舞台。当代码被经常调用时,可以使用收集的统计信息对C2进行优化和编译。
举个例子,我将测试下一个代码片段(我的JDK设置为slowdebug build 9-internal):
public class Demo {
private static void run() {
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
}
System.out.println("Done!");
}
}
使用以下命令行选项:
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Demo.run
我的run方法有不同的版本,适当地用C1和C2编译。对我来说,最终的变体(C2)看起来像这样:
...
; B1: # B3 B2 <- BLOCK HEAD IS JUNK Freq: 1
0x00000000125461b0: mov dword ptr [rsp+0ffffffffffff7000h], eax
0x00000000125461b7: push rbp
0x00000000125461b8: sub rsp, 40h
0x00000000125461bc: mov ebp, dword ptr [rdx]
0x00000000125461be: mov rcx, rdx
0x00000000125461c1: mov r10, 57fbc220h
0x00000000125461cb: call indirect r10 ; *iload_1
0x00000000125461ce: cmp ebp, 7fffffffh ; 7fffffff => 2147483647
0x00000000125461d4: jnl 125461dbh ; jump if not less
; B2: # B3 <- B1 Freq: 0.999999
0x00000000125461d6: mov ebp, 7fffffffh ; *if_icmpge
; B3: # N44 <- B1 B2 Freq: 1
0x00000000125461db: mov edx, 0ffffff5dh
0x0000000012837d60: nop
0x0000000012837d61: nop
0x0000000012837d62: nop
0x0000000012837d63: call 0ae86fa0h
...
它有点乱,但如果你仔细观察,你可能会注意到这里没有长时间运行的循环。有3个区块:B1,B2和B3,执行步骤可以是B1 -> B2 -> B3
或B1 -> B3
。其中Freq: 1
- 规范化块执行的估计频率。
您正在测量检测循环不执行任何操作所花费的时间,在后台线程中编译代码并消除代码。
for (int t = 0; t < 5; t++) {
long start = System.nanoTime();
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
}
long time = System.nanoTime() - start;
String s = String.format("%d: Took %.6f ms", t, time / 1e6);
Thread.sleep(50);
System.out.println(s);
Thread.sleep(50);
}
如果你使用-XX:+PrintCompilation
运行它,你可以看到代码已经在后台编译为3级或C1编译器,并在几次循环后编译到C4的4级。
129 34 % 3 A::main @ 15 (93 bytes)
130 35 3 A::main (93 bytes)
130 36 % 4 A::main @ 15 (93 bytes)
131 34 % 3 A::main @ -2 (93 bytes) made not entrant
131 36 % 4 A::main @ -2 (93 bytes) made not entrant
0: Took 2.510408 ms
268 75 % 3 A::main @ 15 (93 bytes)
271 76 % 4 A::main @ 15 (93 bytes)
274 75 % 3 A::main @ -2 (93 bytes) made not entrant
1: Took 5.629456 ms
2: Took 0.000000 ms
3: Took 0.000364 ms
4: Took 0.000365 ms
如果你改变循环使用long
它没有得到优化。
for (long i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
}
相反,你得到
0: Took 1579.267321 ms
1: Took 1674.148662 ms
2: Took 1885.692166 ms
3: Took 1709.870567 ms
4: Took 1754.005112 ms
您可以考虑以纳秒为单位的开始和结束时间,然后除以10 ^ 6来计算延迟
long d = (finish - start) / 1000000
它应该是10^9
因为1
第二= 10^9
纳秒。
以上是关于为什么40亿次迭代的Java循环只需2 ms?的主要内容,如果未能解决你的问题,请参考以下文章