为什么一个40亿迭代的Java循环仅花费2毫秒?


113

我在装有2.7 GHz Intel Core i7的笔记本电脑上运行以下Java代码。我打算让它测量完成2 ^ 32次迭代的循环所需的时间,我预计大约需要1.48秒(4 / 2.7 = 1.48)。

但是实际上只需要2毫秒,而不是1.48 s。我想知道这是否是底层任何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);
}

69
嗯,是。因为循环体没有副作用,所以编译器很乐意消除了它。检查字节码javap -v以查看。
Elliott Frisch

36
您不会在字节码中看到它。javac几乎没有进行实际优化,而将大部分工作留给了JIT编译器。
乔恩·韦尔尼

4
“我想知道这是否是底层任何JVM优化的结果?” - 你怎么看?如果不是JVM优化,那还能是什么?
apangin

7
这个问题的答案基本上包含在stackoverflow.com/a/25323548/3182664中。它还包含JIT针对此类情况生成的结果程序集(机器代码),表明JIT 完全优化了循环。(stackoverflow.com/q/25326377/3182664上的问题表明,如果循环不执行40亿次操作,而是执行40亿次减一,则可能会花费更长的时间;))我几乎会将这个问题视为另一个问题的副本-有异议吗?
Marco13

7
您假设处理器将对每个Hz执行一次迭代。这是一个深远的假设。正如@Rahul提到的那样,当今的处理器执行各种优化,除非您对Core i7的工作原理有更多了解,否则您将无法假设这一点。
Tsahi Asher

Answers:


106

这里有两种可能性之一:

  1. 编译器意识到循环是多余的,什么也不做,因此优化了它。

  2. JIT(即时编译器)意识到循环是多余的,无所事事,因此对其进行了优化。

现代的编译器非常聪明。他们可以看到代码何时无用。尝试将一个空循环放入GodBolt并查看输出,然后打开-O2优化,您将看到输出类似于

main():
    xor eax, eax
    ret

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


是否允许编译器进行此类优化?对于Java,我不确定,但是.NET编译器通常应避免这种情况,以使JIT对该平台进行最佳优化。
IllidanS4希望莫妮卡

1
@ IllidanS4通常,这取决于语言标准。如果编译器可以执行优化,这意味着标准解释的代码具有相同的效果,则可以。尽管有许多微妙之处需要考虑,例如浮点计算的某些转换可能导致引入上溢/下溢的可能性,因此任何优化都必须谨慎进行。
user1997744

9
@ IllidanS4运行时环境应该如何进行更好的优化?至少它必须分析代码,该代码不能比在编译期间删除代码更快。
Gerhardh

2
@Gerhardh我并不是在谈论这种确切的情况,当运行时无法在删除多余的代码部分方面做得更好时,但是当然在某些情况下,这种原因是正确的。并且由于可以有其他语言的JRE编译器,所以运行时也应该进行这些优化,因此运行时和编译器都没有理由这样做。
IllidanS4希望莫妮卡回到

6
@ IllidanS4任何运行时优化都不能少于零时间。阻止编译器删除代码没有任何意义。
Gerhardh

55

看来它已被JIT编译器优化。当我将其关闭(-Djava.compiler=NONE)时,代码运行会慢很多:

$ javac MyClass.java
$ java MyClass
Used 4
$ java -Djava.compiler=NONE MyClass
Used 40409

我将OP的代码放在中class MyClass


2
奇怪的。当我运行的代码两种方式,它没有标志快,但只有10倍,以及添加或删除零在循环也迭代次数影响十个因素的运行时间,与不旗。所以(对我而言)循环似乎并没有完全优化,只是以某种方式使速度提高了10倍。(Oracle Java 8-151)
tobias_k

@tobias_k这取决于循环正在经历JIT的哪个阶段,我想stackoverflow.com/a/47972226/1059372
Eugene

21

我只想指出一个明显的事实-这是发生的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;
    }
}

结果表明,根据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 

13

如前所述,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-标准化的块执行估计频率。


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);
}

如果与之一起运行,则-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

这很奇怪……为什么long计数器会阻止相同的优化发生?
Ryan Amos

@RyanAmos仅当类型intnote char和short在字节码级别上实际上相同时,优化才应用于公共基本循环计数。
彼得·劳瑞

-1

您考虑开始和完成时间(以纳秒为单位),然后除以10 ^ 6以计算延迟

long d = (finish - start) / 1000000

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


您的建议与我的观点无关。我想知道的是它花费了多长时间,而这个持续时间是以毫秒还是秒为单位打印/表示都没有关系。
twimo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.