2

正如我从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 之后放置条件会阻止我们在每个循环之后执行 goto,这对我来说看起来很合乎逻辑。那么为什么 OracleJDK 会采取另一种方式呢?

4

3 回答 3

2

这不是为了更好的 JIT 优化 - 对于 JIT,这些代码片段是等价的。这是因为在 javac 中进行优化没有意义,因为 JIT 优化更强大。

于 2013-04-11T11:17:12.747 回答
2

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

  1. 初始化
  2. 条件 - 如果为假,则转到 6。
  3. 循环体
  4. 增量
  5. 转到 2
  6. 结尾

这实际上正是编译器在您的情况下生成的内容:

  1. 初始化

    0:   iconst_0
    1:   istore_1
    
  2. 条件 - 如果为假,则转到 6。

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

  4. 增量

    8:   iinc    1, 1
    
  5. 转到 2

    11:  goto    2
    
  6. 结尾

    14:  return
    

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

于 2013-04-11T19:45:01.190 回答
1

无论哪种方式,它的指令数量都是相同的,并且正如已经指出的那样,规范并没有将编译器与这个特定的字节码联系起来:

编译器可能会将自旋编译为...

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

在 body 之后放置条件会阻止我们在每个循环之后执行 goto,这对我来说看起来很合乎逻辑。那么为什么 OracleJDK 会采取另一种方式呢?

除非编写该编译器的那个人摇摆不定,否则可能无法确定地说-但是我想您的理论可能是正确的,因为这似乎与帮助以后的 JIT 优化有关。我唯一的预感(这可能是不正确的)是它可能与goto命令的定位有关 - 如果前 6 条指令被视为一个逻辑块,没有 goto,则可能有更多的潜力来实现更好的代码内联,因为它们位于编译器实际生成的字节码中。当然,由于那个特殊的goto只能跳转到该块内,没有逻辑差异,JIT 仍然可以内联它,效果完全相同。这些天来,我想它足够聪明来解决这个问题,但可能并非总是如此,在这种情况下,生成的代码提供了一个很好的解决方法。当然,当(如果)JIT 变得足够聪明时,就不需要更改代码,所以它卡住了。

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

另一种可能性是编译器的那部分是如何编写的,完全是巧合,根本没有固定的理由(因为在编译器中,开发人员可能没有试图让代码完全像它那样在示例中做了。)

于 2013-04-11T11:17:16.010 回答