JVM 的任何编译器都使用 "wide" goto 吗?

Do any compilers for the JVM use the "wide" goto?

我想你们大多数人都知道 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 字节的分支偏移量。规范指出

Although the goto_w instruction takes a 4-byte branch offset, other factors limit the size of a method to 65535 bytes (§4.11). This limit may be raised in a future release of the Java Virtual Machine.

在我看来 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.

我还没有实际尝试过,因为我可能在更改 "line numbers" 以适应 goto_w 时犯了一个错误。但是因为它在规范中,所以应该可以做到。

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

方法代码最大可达64K

short 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
          ...

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

在查看像 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
…

似乎在某些编译器中(在 1.6.0 和 11.0.7 中尝试过),如果一个方法足够大而永远需要 goto_w,它会独占地使用 goto_w。即使它有非常局部的跳跃,它仍然使用 goto_w.