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

Alo*_*rte 55 java jvm goto

我想你们中的大多数人都知道这goto是 Java 语言中的保留关键字,但实际上并未使用。您可能还知道这goto是一个 Java 虚拟机 (JVM) 操作码。我认为所有的Java,Scala和科特林的复杂的控制流结构,在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想到,也许可以将两个更重要的字节归零,两个不太重要的字节与 for 相同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.
Run Code Online (Sandbox Code Playgroud)

我们可以将其重写为

     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.
Run Code Online (Sandbox Code Playgroud)

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

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

apa*_*gin 54

方法代码的大小可以高达 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 ...
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

反编译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
          ...
Run Code Online (Sandbox Code Playgroud)

  • 凉爽的。谢谢。我想 64k 对于任何人来说都应该足够了。;) (20认同)
  • @ElliottFrisch 确实如此。只要方法的字节码大小不超过65535,并且常量池长度也小于65535。 (3认同)
  • @ElliottFrisch - *提示参考帽子。* (3认同)

Hol*_*ger 36

没有理由使用goto_w时的分支融入一个goto。但是您似乎忽略了分支是相对的,使用带符号的偏移量,因为分支也可以向后移动。

在查看诸如 之类的工具的输出时,您不会注意到它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() {}
Run Code Online (Sandbox Code Playgroud)

在 Ideone 上演示

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() {}
Run Code Online (Sandbox Code Playgroud)

  • @ElliottFrisch我只需要记住“finally”块在正常和异常流程中都会被复制(从Java 6开始是强制性的)。所以嵌套十个意味着×2^1⁰,那么,switch总是有一个默认目标,所以加上iload,它需要十个字节加上填充。我还在每个分支中添加了一个重要的语句以防止优化。利用限制是一个反复出现的主题,[嵌套表达式](/sf/answers/2177858231/)、[lambdas](/sf/answers/2860689401/)、[字段](https:// stackoverflow.com/a/48022050),[构造函数](/sf/answers/3761983361/)... (14认同)
  • 哇,太棒了。我最大的 Java 项目包含几个包和几十个类,编译后几乎有 200KB。但是带有 `methodWithLargeJump()` 的 `Main` 编译后几乎达到 400KB。 (7认同)
  • 这展示了 Java 针对常见情况进行了多少优化...... (4认同)
  • 有趣的是,嵌套表达式和大量构造函数也会遇到编译器实现限制,而不仅仅是字节码限制。还有一个关于[最大类文件大小]的问答(/sf/ask/2960649891/)(也许我在写这个答案时不自觉地记住了Tagir的答案)。最后,[最大包名称长度](/sf/answers/1578809081/),在 JVM 端,[最大嵌套同步](/sf/answers/3843634231/)。看来,人们一直保持着好奇心。 (2认同)

小智 5

目前看来,在一些编译器(在1.6.0和11.0.7试过),如果一个方法是足够大的永远需要goto_w,它采用专门goto_w。即使它有非常局部的跳转,它仍然使用 goto_w。


归档时间:

查看次数:

2209 次

最近记录:

5 年,10 月 前