JVM 的任何编译器都使用 "wide" goto 吗?
Do any compilers for the JVM use the "wide" goto?
我想你们大多数人都知道 goto
是 Java 语言中的保留关键字,但实际上并未使用。您可能还知道 goto
是一个 Java 虚拟机 (JVM) 操作码。我认为 Java、Scala 和 Kotlin 所有复杂的控制流结构,在 JVM 级别,都是使用 goto
和 ifeq
、ifle
、iflt
,等等
查看 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() {}
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.
我想你们大多数人都知道 goto
是 Java 语言中的保留关键字,但实际上并未使用。您可能还知道 goto
是一个 Java 虚拟机 (JVM) 操作码。我认为 Java、Scala 和 Kotlin 所有复杂的控制流结构,在 JVM 级别,都是使用 goto
和 ifeq
、ifle
、iflt
,等等
查看 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() {}
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.