JVMのコンパイラは「ワイド」gotoを使用しますか?


47

gotoJava言語では予約済みのキーワードですが、実際には使用されていません。そして、あなたはおそらくそれ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_wgoto、必要に応じて調整を行って、上位2バイトをゼロにし、下位2バイトをと同じように使用して、必要に応じて調整することもできます。

たとえば、次の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 に対応するために「行番号」の変更を誤った可能性があるため、実際にはこれを試していません。しかし、それは仕様に含まれているので、それを行うことが可能であるはずです。

私の質問は、コンパイラーまたはバイトコードのその他のジェネレーターがgoto_w現在の65535の制限で使用できる理由があるかどうか、それが実行できることを示すこと以外にあるのかどうかです。

回答:


51

メソッドコードのサイズは、最大64Kです。

shortのブランチオフセットは、goto-32768〜32767の符号付き16ビット整数です。

したがって、短いオフセットでは、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 ...コンパイルできますか?単一のソースクラスのサイズには制限があることはわかっていますが、正確に何なのかはわかりません(コード生成は、何かが実際にヒットするのを見たときだけです)。
エリオットフリッシュ

3
@ElliottFrischします。メソッドのバイトコードサイズが65535を超えず、定数プール長も65535未満である
限り

18
涼しい。ありがとう。64kは私が推測する誰にとっても十分であるべきです。;)
エリオットフリッシュ

3
@ElliottFrisch- 参考になるヒント。
TJクラウダー

34

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() {}

Ideoneのデモ

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

7
わあ、すごい。私の最大のJavaプロジェクトは、いくつかのパッケージとそれらの間の数十のクラスで、ほぼ200KBにコンパイルされます。しかし、Mainwith methodWithLargeJump()はほぼ400KBにコンパイルされます。
アロンソデルアルテ

4
これは、Javaが一般的なケースに対してどの程度最適化されているかを示しています...
Holger

1
ジャンプテーブルの乱用をどのようにして発見しましたか?機械生成コード?
エリオットフリッシュ

14
@ElliottFrisch finally通常のフローと例外的なフローではブロックが重複することを思い出すだけで済みました(Java 6以降必須)。したがって、10個のネストは×2×を意味します。その場合、スイッチには常にデフォルトのターゲットがあるため、iloadとともに10バイトとパディングが必要です。また、最適化を防ぐために、各ブランチに重要なステートメントを追加しました。定期的なトピックが制限をされて利用して、ネストされた表現ラムダフィールドコンストラクタ ...
ホルガー

2
興味深いことに、ネストされた式と多くのコンストラクターは、バイトコードの制限だけでなく、コンパイラーの実装の制限にも影響を与えます。また、最大クラスファイルサイズに関するQ&Aもありました(おそらく、この回答を書いているときに、無意識のうちにタギルの回答を思い出しました)。最後に、パッケージ名の最大長と、JVM側では、入れ子の同期の最大。人々は好奇心を持ち続けているようです。
Holger

5

一部のコンパイラー(1.6.0および11.0.7で試行)では、メソッドが十分に大きく、goto_wが必要な場合は、goto_w のみを使用するようです。非常にローカルなジャンプがある場合でも、goto_wを使用します。


1
なぜでしょうか?命令キャッシングと関係がありますか?
アレクサンダー-

@ Alexander-ReinstateMonicaおそらく実装の容易さ。
デビッドG.
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.