为什么一个40亿次迭代的 Java 循环只需要2毫秒?

我在一台2.7 GHz 英特尔酷睿 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);
}
13219 次浏览

有两种可能性:

  1. 编译器意识到这个循环是冗余的,什么也不做,所以它对它进行了优化。

  2. 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

我把行动代码放进了 class MyClass

我只是陈述一个显而易见的事实——这是一个发生的 JVM 优化,循环将被完全删除。下面是一个小测试,它显示了在仅对 C1 Compiler启用/启用并完全禁用时 huge差异 JIT的情况。

免责声明: 不要编写这样的测试-这只是为了证明实际的循环“删除”发生在 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 编译器: C1C2。首先,用 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

我的 快跑方法有不同的版本,用 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 -> B3B1 -> 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级。

    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

You consider start and finish time in nanosecond and you divide by 10^6 for calculate the latency

long d = (finish - start) / 1000000

它应该是 10^9,因为 1秒 = 10^9纳秒。