是否有任何用于 JVM 的编译器使用“宽”转到?

2022-08-31 17:32:46

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

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

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

在我看来,这听起来像是面向未来的,就像其他一些操作码一样。但是我也想到,也许可以与两个更重要的字节清零和两个不太重要的字节一起使用,与 for 相同,并根据需要进行调整。goto_w*_wgoto_wgoto

例如,给定这个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.

我实际上还没有尝试过这个,因为我可能犯了一个错误,更改了“行号”以适应s。但是由于它在规范中,因此应该可以做到这一点。goto_w

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


答案 1

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

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

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

甚至有时发出 .下面是一个示例:javacgoto_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
          ...

答案 2

当分支适合 .但是你似乎忽略了分支是相对的,使用有符号偏移量,因为分支也可以向后移动。goto_wgoto

在查看工具(如 )的输出时,您不会注意到它,因为它会在打印之前计算生成的绝对目标地址。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() {}

Ideone 上的演示

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

推荐