在Java中编译循环



我可以从JVM规范中看到这段代码:

void spin() {
    int i;
    for (i = 0; i < 100; i++) {
        ;    // Loop body is empty
    }
}

应该编译成:

0   iconst_0
1   istore_1
2   goto 8
5   iinc 1 1
8   iload_1
9   bipush 100
11  if_icmplt 5
14  return  

其中条件检查if_icmplt在循环体之后,但是当我自己编译并使用javap查看时,我看到:

0:   iconst_0
1:   istore_1
2:   iload_1
3:   bipush  100
5:   if_icmpge       14
8:   iinc    1, 1
11:  goto    2
14:  return

,循环条件在循环体之前。为什么会发生这种情况?

在body之后放置condition可以防止我们在每个循环之后执行goto,这在我看来是合乎逻辑的。那么,为什么OracleJDK采取另一种方式呢?

这不是为了更好地优化JIT—对于JIT,这些代码片段是等效的。这是因为在javac中进行优化没有意义,因为JIT优化无论如何都更强大。

实现for循环的直接方法(从它的JLS语义)是这样的:

    <
  1. 初始化/gh>
  2. condition -如果false,转到6。
  3. 循环体
  4. 增量
  5. goto 2

这实际上就是编译器在你的情况下生成的:

  1. 初始化
    0:   iconst_0
    1:   istore_1
    
  2. condition -如果为false,转到6。

    2:   iload_1
    3:   bipush  100
    5:   if_icmpge       14
    
  3. 循环体(空)

  4. 增量

    8:   iinc    1, 1
    
  5. goto 2

    11:  goto    2
    
  6. 结束
    14:  return
    

JVM规范中的版本是实现它的另一种方式,这也是可以接受的。正如回答者所说,普通VM的JIT编译器会再次查看并优化它(如果它足够相关的话),所以在字节码级别上的轻微优化只对没有JIT并且逐个指令解释字节码的jvm有影响。即使这样,任何优化也将特定于实际的机器。

这两种方式的指令数都是相同的,正如已经指出的,规范并没有将编译器绑定到这个特定的字节码:

编译器可能会编译spin to…

编译器几乎可以选择将其编译成它想要的任何字节码,只要最终效果相同。

在body之后放置condition可以防止我们在每个循环之后执行goto,这在我看来是合乎逻辑的。那么,为什么OracleJDK采取另一种方式呢?

可能不可能确切地说,除非写编译器的家伙摆动-但是我想你的理论可能是正确的,因为它似乎可能与帮助以后的JIT优化有关。我唯一的预感(这可能是不正确的)是,它可能与goto命令的定位有关-如果将前6条指令作为一个逻辑块,没有gotos,因为它们在编译器实际产生的字节码中,那么可能会有更好的代码内联。当然,由于特定的goto只能跳转到该块内,因此没有逻辑上的区别,JIT仍然可以以完全相同的效果内联它。现在,我认为它足够聪明,可以解决这个问题,但它可能并不总是这样,在这种情况下,生成的代码提供了一个很好的解决方案。当然,当(如果)JIT变得足够聪明时,就不需要修改代码了,所以它就卡住了。

一个详尽的理论,也许是完全错误的-但如果它是一个功能上的差异,这是我最好的猜测!

另一种可能是这部分编译器是怎么写的,完全是巧合,根本没有固定的原因(因为编译器开发者可能没有试图使代码完全像示例中那样)

最新更新