为什么40亿次迭代的Java循环只需2 ms?

twi*_*imo 113 java jvm for-loop

我在配备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);
}
Run Code Online (Sandbox Code Playgroud)

van*_*nch 106

这里有两种可能性之一:

  1. 编译器意识到循环是冗余的,什么都不做,所以它优化了它.

  2. JIT(即时编译器)意识到循环是冗余的,什么都不做,所以它优化了它.

现代编译器非常聪明; 他们可以看到代码何时无用.尝试将一个空循环放入GodBolt并查看输出,然后启用-O2优化,您将看到输出是沿着

main():
    xor eax, eax
    ret
Run Code Online (Sandbox Code Playgroud)

我想澄清一下,在Java中,大多数优化都是由JIT完成的.在其他一些语言(如C/C++)中,大多数优化都是由第一个编译器完成的.

  • @IllidanS4运行时环境应该如何能够做更好的优化?至少它必须分析代码,这不能比编译期间删除代码更快. (9认同)
  • @IllidanS4任何运行时优化都不能少于零时间.防止编译器删除代码没有任何意义. (6认同)
  • @Gerhardh我没有谈论这个精确的情况,当运行时无法更好地删除冗余的代码部分时,但当然有些情况可能是正确的.并且因为其他语言可以有其他JRE编译器,运行时*也应该进行这些优化,因此运行时和编译器都没有理由这样做. (2认同)

Aka*_*all 55

看起来它被JIT编译器优化了.当我关闭它(-Djava.compiler=NONE)时,代码运行得慢得多:

$ javac MyClass.java
$ java MyClass
Used 4
$ java -Djava.compiler=NONE MyClass
Used 40409
Run Code Online (Sandbox Code Playgroud)

我把OP的代码放在里面class MyClass.

  • 奇怪的.当我以两种方式运行代码时,它在没有标志的情况下更快,但只有10倍,并且在循环中向迭代次数添加或删除零也会影响运行时间因子为10,有和没有旗.所以(对我来说)循环似乎没有完全优化,只是以某种方式快10倍.(Oracle Java 8-151) (2认同)

Eug*_*ene 20

我只是陈述了显而易见的事实 - 这是一个JVM优化发生,循环将完全删除.这是一个小测试,显示了仅启用/启用和禁用时的巨大差异.JITC1 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

结果显示,取决于启用哪个部分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 
Run Code Online (Sandbox Code Playgroud)


Ole*_*hov 12

正如已经指出的那样,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!");
    }
}
Run Code Online (Sandbox Code Playgroud)

使用以下命令行选项:

-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*Demo.run
Run Code Online (Sandbox Code Playgroud)

我的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

...
Run Code Online (Sandbox Code Playgroud)

它有点乱,但如果你仔细观察,你可能会注意到这里没有长时间运行的循环.有3个块:B1,B2和B3,执行步骤可以是B1 -> B2 -> B3B1 -> B3.其中Freq: 1- 块执行的归一化估计频率.


Pet*_*rey 8

您正在测量检测循环不执行任何操作所花费的时间,在后台线程中编译代码并消除代码.

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);
}
Run Code Online (Sandbox Code Playgroud)

如果你运行它,-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
Run Code Online (Sandbox Code Playgroud)

如果你改变循环使用long它不会得到优化.

    for (long i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
    }
Run Code Online (Sandbox Code Playgroud)

相反,你得到

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
Run Code Online (Sandbox Code Playgroud)