web-dev-qa-db-ja.com

invokedynamicを使用してJava 8ラムダが呼び出されるのはなぜですか?

invokedynamic命令は、VMがコンパイル時にメソッド参照をハードワイヤリングする代わりに、実行時にメソッド参照を決定するのに役立ちます。

これは、正確なメソッドと引数の型が実行時までわからない動的言語で役立ちます。しかし、Javaラムダの場合はそうではありません。それらは明確に定義された引数を持つ静的メソッドに変換されます。このメソッドはinvokestaticを使用して呼び出すことができます。

では、特にパフォーマンスに影響がある場合、ラムダのinvokedynamicの必要性は何ですか?

62
Kshitiz Sharma

ラムダはinvokedynamicを使用して呼び出されず、オブジェクト表現はinvokedynamicを使用して作成され、実際の呼び出しは通常のinvokevirtualまたはinvokeinterfaceです。

例えば:

// creates an instance of (a subclass of) Consumer 
// with invokedynamic to Java.lang.invoke.LambdaMetafactory 
something(x -> System.out.println(x));   

void something(Consumer<String> consumer) {
      // invokeinterface
      consumer.accept("hello"); 
}

ラムダは、何らかの基本クラスまたはインターフェイスのインスタンスになる必要があります。そのインスタンスには、元のメソッドからキャプチャされた変数のコピーと、親オブジェクトへのポインタが含まれることがあります。これは、匿名クラスとして実装できます。

なぜinvokedynamic

簡単な答えは、実行時にコードを生成することです。

Javaメンテナーは、実行時に実装クラスを生成することを選択しました。これは、Java.lang.invoke.LambdaMetafactory.metafactory。その呼び出しの引数(戻り値の型、インターフェイス、およびキャプチャされたパラメーター)は変更される可能性があるため、これにはinvokedynamicが必要です。

invokedynamicを使用して実行時に匿名クラスを構築すると、JVMが実行時にそのクラスバイトコードを生成できます。同じステートメントに対する後続の呼び出しでは、キャッシュバージョンが使用されます。 invokedynamicを使用するもう1つの理由は、既にコンパイルされたコードを変更することなく、将来的に実装戦略を変更できるようにするためです。

撮影されていない道路

もう1つのオプションは、上記のコードを以下に変換するのと同等の、各ラムダインスタンス化のインナークラスを作成するコンパイラーです。

something(new Consumer() { 
    public void accept(x) {
       // call to a generated method in the base class
       ImplementingClass.this.lambda$1(x);

       // or repeating the code (awful as it would require generating accesors):
       System.out.println(x);
    }
);   

これには、コンパイル時にクラスを作成し、実行時にロードする必要があります。 jvmの動作方法では、これらのクラスは元のクラスと同じディレクトリに存在します。そして、そのラムダを使用するステートメントを初めて実行するとき、その匿名クラスをロードして初期化する必要があります。

パフォーマンスについて

invokedynamicへの最初の呼び出しは、匿名クラスの生成をトリガーします。次に、オペコードinvokedynamiccode に置き換えられます。これは、匿名インスタンス化を手動で記述するのとパフォーマンスが同等です。

67
Daniel Sperry

Brain Goetzは、ラムダ翻訳戦略の理由を 彼の論文 の1つで説明しましたが、残念ながら今は利用できないようです。幸いなことに、私はコピーを保管しました:

翻訳戦略

内部クラス、メソッドハンドル、動的プロキシなど、バイトコードでラムダ式を表す方法はいくつかあります。これらのアプローチにはそれぞれ長所と短所があります。戦略の選択には、競合する2つの目標があります。特定の戦略をコミットしないことで将来の最適化の柔軟性を最大化することと、クラスファイル表現の安定性を提供することです。 JSR 292のinvokedynamic機能を使用して、バイトコードでのラムダ作成のバイナリ表現を、実行時にラムダ式を評価するメカニズムから分離することにより、これらの両方の目標を達成できます。バイトコードを生成してラムダ式を実装するオブジェクト(内部クラスのコンストラクターの呼び出しなど)を作成する代わりに、ラムダを構築するためのレシピを記述し、実際の構築を言語ランタイムに委任します。そのレシピは、invokedynamic命令の静的および動的引数リストにエンコードされます。

Invokedynamicを使用すると、実行時まで翻訳戦略の選択を延期できます。ランタイムの実装は、ラムダ式を評価するために戦略を動的に選択できます。ランタイム実装の選択は、ラムダ構築のための標準化された(つまり、プラットフォーム仕様の一部)APIの背後に隠されているため、静的コンパイラーはこのAPIの呼び出しを発行でき、JRE実装は好みの実装戦略を選択できます。 invokedynamicメカニズムにより、この遅延バインディングアプローチが課すパフォーマンスコストなしでこれを実行できます。

コンパイラは、ラムダ式に遭遇すると、まずラムダ本体をラムダ式の引数リストと戻り値の型に一致するメソッドに下げます(デシュガーします)。 )ラムダ式がキャプチャされるポイントで、invokedynamic呼び出しサイトを生成し、呼び出されると、ラムダが変換される機能インターフェイスのインスタンスを返します。この呼び出しサイトは、特定のラムダのラムダファクトリと呼ばれます。ラムダファクトリへの動的な引数は、レキシカルスコープからキャプチャされた値です。ラムダファクトリのbootstrapメソッドは、ラムダメタファクトリーと呼ばれる言語ランタイムライブラリであるJavaの標準化されたメソッドです。staticbootstrap引数は、コンパイル時にラムダについて知られている情報をキャプチャします(変換先の機能的インターフェイス、脱糖されたラムダ本体のメソッドハンドル、SAMタイプがシリアル化可能かどうかなどの情報)

メソッド参照はラムダ式と同じように扱われますが、ほとんどのメソッド参照は新しいメソッドにデシュガーする必要はありません。参照されたメソッドの定数メソッドハンドルをロードし、それをメタファクトリーに渡すだけです。

したがって、ここでの考え方は、翻訳戦略をカプセル化し、それらの詳細を隠して特定の方法を実行することではないように思われました。将来、型の消去と値型の欠如が解決され、多分Javaが実際の関数型をサポートするようになったとき、彼らはそこに行き、問題を引き起こすことなく別の戦略を変更するかもしれませんユーザーのコード。

43
Edwin Dalorzo

現在のJava 8のラムダ実装は複合的な決定です。

    1. ラムダ式を包含クラスの静的メソッドにコンパイルします。ラムダを個別の内部クラスファイルにコンパイルする代わりに(Scalaはこのようにコンパイルするため、たくさんの$$$クラスファイルがあります)
    1. 定数プールの導入:BootstrapMethods。静的メソッド呼び出しを呼び出しサイトオブジェクトにラップします(後で使用するためにキャッシュできます)

あなたの質問に答えるために、

    1. invokedynamicを使用した現在のラムダ実装は、これらの内部クラスファイルをロードする必要はなく、代わりにオンザフライで内部クラスbyte []を作成するため(たとえばFunctionインターフェースを満たすため)、別個の内部クラス方法よりも少し高速です。後で使用するためにキャッシュされます。
    1. JVMチームは、独立した内部クラス(外側のクラスの静的メソッドを参照)ファイルを生成することを選択する場合がありますが、柔軟性があります
5
fjolt