1. 概要

コンパイラーとランタイムは、最も小さく、一見それほど重要ではない部分でさえ、すべてを最適化する傾向があります。 この種の最適化に関しては、JVMとJavaには多くのメリットがあります。

この記事では、これらの比較的新しい最適化の1つである stringconcatenationinvokedynamicを評価します。

2. Java9より前

Java 9より前は、StringBuilderを使用して重要な文字列連結が実装されていました。 たとえば、次の方法を考えてみましょう。

String concat(String s, int i) {
    return s + i;
}

この単純なコードのバイトコードは次のとおりです( javap -c を使用)。

java.lang.String concat(java.lang.String, int);
  Code:
     0: new           #2      // class StringBuilder
     3: dup
     4: invokespecial #3      // Method StringBuilder."<init>":()V
     7: aload_0
     8: invokevirtual #4      // Method StringBuilder.append:(LString;)LStringBuilder;
    11: iload_1
    12: invokevirtual #5      // Method StringBuilder.append:(I)LStringBuilder;
    15: invokevirtual #6      // Method StringBuilder.toString:()LString;

ここで、Java8コンパイラはStringBuilder を使用して、コードで StringBuilder を使用していなくても、メソッド入力eを連結しています。

公平を期すために、StringBuilderを使用した文字列の連結は、非常に効率的で適切に設計されています。

Java 9がこの実装をどのように変更するか、およびそのような変更の動機は何であるかを見てみましょう。

3. ダイナミックを呼び出す

Java 9以降、 JEP 280 の一部として、文字列連結はinvokedynamicを使用するようになりました。

変更の背後にある主な動機は、より動的な実装を行うことです。 つまり、バイトコードを変更せずに連結戦略を変更することが可能です。 このようにして、クライアントは、再コンパイルしなくても、新しい最適化された戦略の恩恵を受けることができます。

他にも利点があります。 たとえば、 invokedynamic のバイトコードは、よりエレガントで、もろくなく、小さくなっています。

3.1. 大局

この新しいアプローチがどのように機能するかを詳しく説明する前に、より広い観点から見てみましょう。

例として、別の文字列 int と結合して、新しい文字列を作成するとします。 これは、文字列とintを受け取り、連結された文字列を返す関数と考えることができます。

この例で新しいアプローチがどのように機能するかを次に示します。

  • 連結を説明する関数シグネチャを準備します。 たとえば、(String、int)-> String
  • 連結の実際の引数を準備します。 たとえば、「答えは「と42」に参加する場合、これらの値は引数になります
  • ブートストラップメソッドを呼び出し、関数のシグネチャ、引数、およびその他のいくつかのパラメータをメソッドに渡します
  • その関数シグネチャの実際の実装を生成し、それをMethodHandle内にカプセル化します
  • 生成された関数を呼び出して、最終的に結合された文字列を作成します

簡単に言えば、 バイトコードは、コンパイル時に仕様を定義します。 次に、ブートストラップメソッドは、実行時に実装をその仕様にリンクします。 これにより、バイトコードに触れることなく実装を変更できるようになります。

この記事全体を通して、これらの各ステップに関連する詳細を明らかにします。

まず、ブートストラップ法へのリンクがどのように機能するかを見てみましょう。

4. リンケージ

Java9+コンパイラが同じメソッドのバイトコードを生成する方法を見てみましょう。

java.lang.String concat(java.lang.String, int);
  Code:
     0: aload_0
     1: iload_1
     2: invokedynamic #7,  0   // InvokeDynamic #0:makeConcatWithConstants:(LString;I)LString;
     7: areturn

素朴なStringBuilderアプローチとは対照的に、これは非常に少ない数の命令を使用しています

このバイトコードでは、(LString; I)LString署名が非常に興味深いものです。 StringintIintを表します)を取り、連結された文字列を返します。 これは、メソッドが1つの文字列intを結合するためです。

他のinvoke動的実装と同様に、ロジックの多くはコンパイル時から実行時に移動されます。

そのランタイムロジックを確認するために、ブートストラップメソッドテーブルを調べてみましょう( javap -c -v を使用)。

BootstrapMethods:
  0: #25 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:
    (Ljava/lang/invoke/MethodHandles$Lookup;
     Ljava/lang/String;
     Ljava/lang/invoke/MethodType;
     Ljava/lang/String;
     [Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #31 \u0001\u0001

この場合、JVMは invokedynamic 命令を初めて検出すると、makeConcatWithConstantsブートストラップメソッドを呼び出します。 次に、ブートストラップメソッドは、連結ロジックを指すConstantCallSiteを返します。

ブートストラップメソッドに渡される引数の中で、2つが際立っています。

  • Ljava / lang / invoke / MethodType は、文字列連結署名を表します。 この場合、整数と String を組み合わせているため、(LString; I)LStringになります。
  • \ u0001 \ u0001 は、文字列を作成するためのレシピです(これについては後で詳しく説明します)

5. レシピ

レシピの役割をよりよく理解するために、単純なデータクラスについて考えてみましょう。

public class Person {

    private String firstName;
    private String lastName;

    // constructor

    @Override
    public String toString() {
        return "Person{" +
          "firstName='" + firstName + '\'' +
          ", lastName='" + lastName + '\'' +
          '}';
    }
}

String 表現を生成するために、JVMはfirstNameおよびlastNameフィールドを引数としてinvokedynamic命令に渡します。

 0: aload_0
 1: getfield      #7        // Field firstName:LString;
 4: aload_0
 5: getfield      #13       // Field lastName:LString;
 8: invokedynamic #16,  0   // InvokeDynamic #0:makeConcatWithConstants:(LString;LString;)L/String;
 13: areturn

今回は、ブートストラップメソッドテーブルが少し異なります。

BootstrapMethods:
  0: #28 REF_invokeStatic StringConcatFactory.makeConcatWithConstants // truncated
    Method arguments:
      #34 Person{firstName=\'\u0001\', lastName=\'\u0001\'} // The recipe

上に示したように、レシピは、連結された Stringの基本構造を表しています。 たとえば、前述のレシピは次のもので構成されています。

  • 「「これらのリテラル値は、連結された文字列にそのまま存在します
  • 通常の引数を表す2つの\u0001タグ。 これらは、firstNameなどの実際の引数に置き換えられます。

レシピは、静的パーツと可変プレースホルダーの両方を含むテンプレート化されたStringと考えることができます。

レシピを使用すると、すべての動的引数と1つのレシピを渡すだけでよいため、ブートストラップメソッドに渡される引数の数を大幅に減らすことができます。

6. バイトコードフレーバー

新しい連結アプローチには、2つのバイトコードフレーバーがあります。 これまでのところ、 makeConcatWithConstantsブートストラップメソッドを呼び出してレシピを渡すという1つのフレーバーに精通しています。 定数付きのインディとして知られるこのフレーバーは、Java9のデフォルトのフレーバーです。

レシピを使用する代わりに、2番目のフレーバーはすべてを引数として渡します。 つまり、定数部分と動的部分を区別せず、それらすべてを引数として渡します。

2番目のフレーバーを使用するには、-XDstringConcat=indyオプションをJavaコンパイラに渡す必要があります。 たとえば、同じ Person クラスをこのフラグでコンパイルすると、コンパイラは次のバイトコードを生成します。

public java.lang.String toString();
    Code:
       0: ldc           #16      // String Person{firstName=\'
       2: aload_0
       3: getfield      #7       // Field firstName:LString;
       6: bipush        39
       8: ldc           #18      // String , lastName=\'
      10: aload_0
      11: getfield      #13      // Field lastName:LString;
      14: bipush        39
      16: bipush        125
      18: invokedynamic #20,  0  // InvokeDynamic #0:makeConcat:(LString;LString;CLString;LString;CC)LString;
      23: areturn

今回のブートストラップ法はmakeConcatです。 さらに、連結署名は7つの引数を取ります。 各引数は、toStringの一部を表します。

  • 最初の引数は、 firstName 変数の前の部分を表します— “ Person {firstName = \’” literal
  • 2番目の引数は、firstNameフィールドの値です。
  • 3番目の引数は一重引用符です
  • 4番目の引数は、次の変数の前の部分です— “、lastName = \’”
  • 5番目の引数はlastNameフィールドです
  • 6番目の引数は一重引用符です
  • 最後の引数は閉じ中括弧です

このように、ブートストラップ法には、適切な連結ロジックをリンクするのに十分な情報があります。

非常に興味深いことに、Java 9より前の世界に戻って、StringBuilderを-XDstringConcat=inlineコンパイラオプションとともに使用することもできます。

7. 戦略

ブートストラップメソッドは、最終的に、実際の連結ロジックを指すMethodHandleを提供します。 この記事の執筆時点では、このロジックを生成するための6つの異なる戦略があります。

  • BC_SBまたは「bytecodeStringBuilder 」戦略は、実行時に同じStringBuilderバイトコードを生成します。 次に、Unsafe.defineAnonymousClassメソッドを介して生成されたバイトコードをロードします
  • BC_SB_SIZED 戦略は、StringBuilderに必要な容量を推測しようとします。 それ以外は、前のアプローチと同じです。 容量を推測すると、 StringBuilder が、基になる byte[]のサイズを変更せずに連結を実行するのに役立つ可能性があります。
  • BC_SB_SIZED_EXACT は、 StringBuilder に基づくバイトコードジェネレーターであり、必要なストレージを正確に計算します。 正確なサイズを計算するには、まず、すべての引数をStringに変換します。
  • MH_SB_SIZEDMethodHandleに基づいており、最終的には StringBuilderAPIを呼び出して連結します。 この戦略はまた、必要な容量についての知識に基づいた推測を行います
  • MH_SB_SIZED_EXACT は、必要な容量を完全な精度で計算することを除いて、前のものと同様です。
  • MH_INLINE_SIZE_EXACT は、必要なストレージを事前に計算し、その byte [] を直接維持して、連結結果を格納します。この戦略は、 StringBuilder [ X225X]は内部的に行います

デフォルトの戦略はMH_INLINE_SIZE_EXACTです。 ただし、この戦略を変更するには、 -Djava.lang.invoke.stringConcat = システムプロパティ。 

8. 結論

この詳細な記事では、新しい String 連結がどのように実装されているか、およびそのようなアプローチを使用する利点について説明しました。

さらに詳細な議論については、実験ノートまたはソースコードをチェックすることをお勧めします。