【问题标题】:Do any compilers for the JVM use the "wide" goto?JVM 的编译器是否使用“宽”goto?
【发布时间】:2020-04-17 22:32:43
【问题描述】:

我想你们中的大多数人都知道goto 是 Java 语言中的保留关键字,但实际上并未使用。您可能还知道goto 是一个Java 虚拟机(JVM)操作码。我认为 Java、Scala 和 Kotlin 的所有复杂控制流结构在 JVM 级别都是使用 gotoifeqifleiflt 等的某种组合来实现的。

查看 JVM 规范 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.goto_w 我看到还有一个 goto_w 操作码。 goto 采用 2 字节的分支偏移量,goto_w 采用 4 字节的分支偏移量。规范指出

虽然 goto_w 指令采用 4 字节的分支偏移量,但其他因素将方法的大小限制为 65535 字节(第 4.11 节)。在 Java 虚拟机的未来版本中可能会提高此限制。

在我看来,goto_w 是面向未来的,就像其他一些 *_w 操作码一样。但我也想到,也许goto_w 可以与两个较高的有效字节清零和两个较低的有效字节与goto 相同,并根据需要进行调整。

例如,给定这个 Java Switch-Case(或 Scala Match-Case):

     12: lookupswitch  {
                112785: 48 // case "red"
               3027034: 76 // case "green"
              98619139: 62 // case "blue"
               default: 87
          }
      48: aload_2
      49: ldc           #17                 // String red
      51: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      54: ifeq          87
      57: iconst_0
      58: istore_3
      59: goto          87
      62: aload_2
      63: ldc           #19                 // String green
      65: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      68: ifeq          87
      71: iconst_1
      72: istore_3
      73: goto          87
      76: aload_2
      77: ldc           #20                 // String blue
      79: invokevirtual #18 
      // etc.

我们可以改写成

     12: lookupswitch  { 
                112785: 48
               3027034: 78
              98619139: 64
               default: 91
          }
      48: aload_2
      49: ldc           #17                 // String red
      51: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      54: ifeq          91 // 00 5B
      57: iconst_0
      58: istore_3
      59: goto_w        91 // 00 00 00 5B
      64: aload_2
      65: ldc           #19                 // String green
      67: invokevirtual #18
            // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      70: ifeq          91
      73: iconst_1
      74: istore_3
      75: goto_w          91
      79: aload_2
      81: ldc           #20                 // String blue
      83: invokevirtual #18 
      // etc.

我实际上并没有尝试过,因为我可能犯了一个错误,更改“行号”以适应 goto_ws。不过既然在spec里面,应该是可以做到的。

我的问题是编译器或其他字节码生成器是否有理由使用具有当前 65535 限制的 goto_w,而不是表明它可以完成?

【问题讨论】:

    标签: java jvm goto


    【解决方案1】:

    方法代码的大小可以大到64K。

    goto 的分支偏移量是一个有符号的 16 位整数:从 -32768 到 32767。

    因此,短偏移量不足以从 65K 方法的开头跳转到结尾。

    即使javac 有时也会发出goto_w。这是一个例子:

    public class WideGoto {
    
        public static void main(String[] args) {
            for (int i = 0; i < 1_000_000_000; ) {
                i += 123456;
                // ... repeat 10K times ...
            }
        }
    }
    

    javap -c反编译:

      public static void main(java.lang.String[]);
        Code:
           0: iconst_0
           1: istore_1
           2: iload_1
           3: ldc           #2
           5: if_icmplt     13
           8: goto_w        50018     // <<< Here it is! A jump to the end of the loop
              ...
    

    【讨论】:

    • // ... repeat 10K times ... 编译?我知道单个源类的大小是有限制的……但我不知道它到底是什么(代码生成是我唯一一次看到有东西真正击中它)。
    • @ElliottFrisch 确实如此。只要方法的字节码大小不超过65535,并且常量池长度也小于65535。
    • 酷。谢谢。我猜对任何人来说64k应该足够了。 ;)
    • @ElliottFrisch - 参考提示。
    【解决方案2】:

    当分支适合goto 时,没有理由使用goto_w。但是您似乎错过了分支是相对,使用带符号的偏移量,因为分支也可以向后移动。

    在查看像 javap 这样的工具的输出时,您不会注意到它,因为它会在打印之前计算得到的绝对目标地址。

    所以goto-327678 … +32767‬ 范围并不总是足以解决0 … +65535 范围内的每个可能的目标位置。

    例如,下面的方法会以goto_w开头的指令:

    public static void methodWithLargeJump(int i) {
        for(; i == 0;) {
            try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
            try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
            try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
            try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
            try {x();} finally { switch(i){ case 1: try {x();} finally { switch(i){ case 1: 
            } } } } } } } } } } } } } } } } } } } } 
        }
    }
    static void x() {}
    

    Demo on Ideone

    Compiled from "Main.java"
    class LargeJump {
      public static void methodWithLargeJump(int);
        Code:
           0: iload_0
           1: ifeq          9
           4: goto_w        57567
    …
    

    【讨论】:

    • 哇,太棒了。我最大的 Java 项目,有几个包和几十个类,编译到将近 200KB。但是你的 MainmethodWithLargeJump() 编译到将近 400KB。
    • 这展示了 Java 针对常见情况进行了多少优化......
    • 您是如何发现跳表滥用的?机器生成的代码?
    • @ElliottFrisch 我只需要记住 finally 块会被复制以用于正常和异常流(自 Java 6 以来是强制性的)。所以嵌套十个意味着×2¹⁰,那么,switch总是有一个默认目标,所以和iload一起,它需要十个字节加上填充。我还在每个分支中添加了一个重要的语句来防止优化。利用限制是一个反复出现的话题,nested expressionslambdasfieldsconstructors...
    • 有趣的是,嵌套表达式和大量构造函数也达到了编译器实现的限制,而不仅仅是字节码的限制。还有一个关于max class file size的Q&A(可能是我写这个答案的时候不知不觉想起了Tagir的答案)。最后是max package name length,在 JVM 端,max nested synchronized。似乎,人们一直保持好奇。
    【解决方案3】:

    似乎在某些编译器中(在 1.6.0 和 11.0.7 中尝试过),如果方法足够大以至于需要 goto_w,它会仅使用 goto_w。即使它有非常局部的跳转,它仍然使用 goto_w。

    【讨论】:

    • 为什么会这样?跟指令缓存有关系吗?
    • @Alexander-ReinstateMonica 可能只是易于实施。
    猜你喜欢
    • 1970-01-01
    • 2011-02-04
    • 2020-10-27
    • 1970-01-01
    • 2011-03-22
    • 2010-11-16
    • 1970-01-01
    • 1970-01-01
    • 2017-05-20
    相关资源
    最近更新 更多