ラムダと invokedynamic の蜜月

  • 4,125 views
Uploaded on

 

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
4,125
On Slideshare
0
From Embeds
0
Number of Embeds
7

Actions

Shares
Downloads
32
Comments
0
Likes
24

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. ラムダと invokedynamic の蜜月 宮川 拓 / @miyakawa_taku 2013-07-22 JJUG ナイトセミナー
  • 2. 自己紹介 • 宮川 拓 (@miyakawa_taku) と申します • SI 屋です • JJUG 幹事です • Kink という JVM 言語を開発しています – https://bitbucket.org/kink/kink 1
  • 3. 要旨 • ラムダ式の実行は invokedynamic で実現さ れます。その理由と、実行の流れを見ます – 論点整理 – ラムダ式の実行 (1) – invokedynamic の復習 – ラムダ式の実行 (2) – なぜ invokedynamic? – ラムダの直列化 2 ※注記 この資料は、 JDK, JRE の「仕様」と「実装」を厳密に区別していません。
  • 4. 論点整理 3
  • 5. 静的構造 4 関数型インタフェース ラムダのクラス ラムダのインスタンス instance-of implements Comparable Comparable<String> c = (x, y) -> x.length() - y.length(); c c のクラス
  • 6. 実行の流れ 5 Comparable<String> c = (x, y) -> x.length() - y.length(); Collections.sort(strings, c); main Collections ラムダ sort compare / 処理の中身の実行 new / ラムダ式の実行 今回の主な論点
  • 7. ラムダ式の実行 (1) 6
  • 8. ラムダのクラスの実行時生成 • 匿名クラスがコンパイル時に生成されるのに 対し、ラムダのクラスは実行時に生成されます。 まずはそれを確かめます 7 関数型インタフェース ラムダのクラス ラムダのインスタンス instance-of implements 実行時に生成
  • 9. 匿名クラスをコンパイル • 匿名クラスは、「外側のクラス名$連番」という 名前で、コンパイラによって生成されます 8 $ cat >AnonClass.java import java.util.*; class AnonClass { Comparator<String> comparator() { return new Comparator<String> { @Override public int compare(String x, String y) { return x.length() – y.length(); } }; } } $ javac AnonClass.java && ls *.class AnonClass$1.class AnonClass.class
  • 10. ラムダをコンパイル • 同等のラムダをコンパイルしても、対応するク ラスファイルは生成されません 9 $ cat >Lambda.java import java.util.*; class Lambda { Comparator<String> comparator() { return (x, y) -> x.length() - y.length(); } } $ javac Lambda.java && ls *.class Lambda.class
  • 11. ラムダのクラスの生成タイミング • ラムダのクラスが、コンパイル時には生成され ないことが分かりました • したがって、実行時のどこかのタイミングで生 成されているはずです 10 ラムダを含む ソース クラス ファイル ラムダの クラスの生成 ラムダの インスタンス化 コンパイル (JDK) 実行 (JVM) この時点では ラムダのクラスは 生成されない どこかの タイミング
  • 12. ラムダのクラスの名前 • まずはラムダのクラスの名前を確認します 11 $ cat >Lambda.java import java.util.*; public class Lambda { public static void main(String[] args) { Comparator<String> c = (x, y) -> x.length() - y.length(); System.out.println(c.getClass()); } } $ javac Lambda.java && java Lambda class Lambda$$Lambda$1
  • 13. 生成のタイミング • loadClass(name) すると、ラムダ式を実行する タイミングでラムダのクラスが出現しているこ とが分かります 12 $ cat >Lambda.java ... System.out.println(loadClassOrNull("Lambda$$Lambda$1")); Comparator<String> c = (x, y) -> x.length() - y.length(); System.out.println(loadClassOrNull("Lambda$$Lambda$1")); ... $ javac Lambda.java && java Lambda null class Lambda$$Lambda$1
  • 14. ラムダ式実行の過程 • ラムダ式を実行するタイミングで、ラムダのク ラスが生成されていることが分かりました • ではラムダ式は、バイトコードのレベルでは、ど のような過程で実行されているのでしょうか? 13
  • 15. ラムダ式のバイトコード • ラムダ式を含むプログラムのクラスファイルを、 javap コマンドで逆アセンブルします 14 $ cat >Lambda.java import java.util.function.*; class Lambda { IntUnaryOperator adder(int delta) { return n -> n + delta; } } $ javap -c -p Lambda.class ... (次ページ) ...
  • 16. javap による逆アセンブルの結果 15 class Lambda { ... java.util.function.IntUnaryOperator adder(int); Code: 0: iload_1 1: invokedynamic #2, 0 6: areturn private static int lambda$0(int, int); Code: 0: iload_1 1: iload_0 2: iadd 3: ireturn }
  • 17. 再度 Java プログラム風に解釈 • ラムダの処理の中身は lambda$0 というメ ソッドに記述されます • ラムダ式の実行は invokedynamic 命令の呼 び出しになっています 16 class Lambda { IntUnaryOperator adder(int delta) { return <invokedynamic>(delta); } private static int lambda$0(int delta, int n) { return n + delta; } }
  • 18. ここまでの整理 • 分かったこと – ラムダのクラスはラムダ式実行の際に生成されま す – ラムダ式の実行は invokedynamic 命令です • 推定できること – invokedynamic 命令をきっかけとして、ラムダの クラスが生成され、またラムダのインスタンスが生 成されるはずです 17
  • 19. invokedynamic の復習 18
  • 20. invokedynamic の復習 • ラムダ式実行の流れを追いかけるにあたり、 まずは invokedynamic をおさらいします 19
  • 21. invokedynamic とは • 本来は、 JRuby など、 Java 以外の言語処理 系のために、 Java SE 7 で追加されたメソッド 呼び出し命令です • invokevirtual, invokeinterface など、 Java SE 6 までのメソッド呼び出し命令と異なり、呼び 出す処理が実行時に選択できます 20
  • 22. Java のメソッド呼び出し手順 • どの呼び出し命令でも、手順は大体同じです 21 int result = receiver.doSomething(arg0, arg1); receiver arg0 receiver arg1 arg0 receiver 戻り値 invokexxx レシーバと引数をスタックに積む 呼び出し 結果も スタックに void 以外の場合
  • 23. Java SE 6 までの呼び出し命令 • Java SE 6 までの呼び出し命令は、いずれも Java 言語と密に結びついてます – メソッドは再定義されない – 名前、引数の型、レシーバのクラスが決まれば、呼 び出すべき処理が定まる 22 invokestatic static メソッドを呼び出す invokespecial コンストラクタ、 private メソッド等を呼び出す invokevirtual クラスに属するメソッドを呼び出す invokeinterface インタフェースに属するメソッドを呼び出す
  • 24. Java 以外の言語処理系の実装 ―Java SE 6 以前 • Java 言語にない機構(メソッド再定義など)を 実現するため、処理系が呼び出しに介入 →JVM による実行時最適化が効きづらい 23 array.join invoke virtual 処理系 size Func@42 join Func@123 検索 def join ... invoke virtual 関数テーブル
  • 25. Java 以外の言語処理系の実装 ―Java SE 7 以降 • invokedynamic を使って、処理系を介さずに、 直接メソッドが呼び出せるようになりました →JVM による実行時最適化が効きやすい! 24 array.join invokedynamic def join ...
  • 26. invokedynamic の道具立て • 呼び出し元 (CallSite) ごとに、ブートストラップ メソッドで、呼び出し先の関数ポインタ (MethodHandle) を登録 25 Method Handle オブジェクト CallSite オブジェクト ブートストラップ メソッド <<create>> 初回呼び出しの前に 実行 対象の 処理 呼び出し元 <<create>> 呼び出し
  • 27. ブートストラップメソッド • static である必要がある • ブートストラップメソッドの引数 – Lookup: MethodHandle のファクトリ – String: 「メソッド名」だが、使わなくても可 – MethodType: invokedynamic の引数型と戻り値型 – 任意個数の定数 • ブートストラップメソッドの戻り値 – MethodHandle の初期値が紐付けられた CallSite 26
  • 28. ブートストラップメソッドの例 • メソッドを呼び出した後、強制的に戻り値を 42 にする MethodHandle を生成 27 static CallSite bsm(Lookup lu, String name, MethodType mt) throws Exception { MethodHandle vmh = lu.findVirtual(mt.parameterType(0), name, vmt); return new ConstantCallSite(filterReturnValue(vmh, dropArguments(constant(int.class, 42), 0, int.class))); } • 以上のように、個々の invokedynamic の動作は、 ブートストラップメソッドを見れば見当が付きます
  • 29. ラムダ式の実行 (2) 28
  • 30. ラムダ式の invokedynamic • 先ほど見たところでは、ラムダ式の実行は、 invokedynamic 命令の実行として実装され ていました →紐付けられているブートストラップメソッドを見 れば、実際の動作がわかるはずです 29
  • 31. ラムダ式の実行の流れ • ラムダ式の実行の invokedynamic には、 java.lang.invoke.LambdaMetaFactory の metaFactory メソッドがブートストラップメソッ ドとして紐付いています 30 invoke dynamic <<ブートストラップ>> LambdaMetaFactory #metaFactory ラムダの インスタンス化 2 回目以降の 実行 1. ラムダのクラスを生成 2. ラムダをインスタンス化する MethodHandle を生成
  • 32. LambdaMetaFactory #metaFactory 31 CallSite metafactory(Lookup lookup, // MethodHandle のファクトリ String name, // 関数型インタフェースの唯一の抽象メソッド (SAM) の名前 (例: compare) MethodType invokedType, // 命令の引数・戻り値型 MethodType samMethod, // SAM の引数・戻り値型 MethodHandle implMethod, // 処理本体のメソッド (例: lambda$0) MethodType instantiatedSamType) // 型パラメータ適用後の SAM の引数・戻り値型 シグネチャ 1. これらの情報を元にラムダのクラスを生成 • 引数をフィールドに格納するコンストラクタ • 処理本体のメソッドを呼び出す SAM の実装 2. ラムダをインスタンス化する MethodHandle を生成、 CallSite に紐付け
  • 33. 最終的に実行される処理 32 class Lambda { static class Lambda$1 implements IntUnaryOperator { private final int delta; Lambda$1(int delta) { this.delta = delta; } @Override public int applyAsInt(int n) { return lambda$0(this.delta, n); } } IntUnaryOperator adder(int delta) { return <invokedynamic: new Lambda$1(delta)>; } private static int lambda$0(int delta, int n) { return n + delta; } } metaFactory が生成 → 結局、やってることは匿名クラスと(ほぼ)同じ!
  • 34. なぜ invokedynamic? 33
  • 35. なぜ invokedynamic? • invokedynamic によるラムダ式の実行は、動 作としては匿名クラスと似たようなものでした • なぜ、わざわざ invokedynamic を使うので しょうか? →(1) クラスファイルが少なくなるおかげで、起動が 速くなる、かもしれません →(2) JVM が LambdaMetaFactory を独自に実装 することで、最適なインスタンス生成の方法を選 択できるようになります 34
  • 36. (1) 起動時間 • Java SE 8 では、 Streams API の採用によって、 プログラム中で全面的にラムダ式が利用され ることが想定されています(実態はどうあれ!) • その際、ラムダ式を匿名クラス方式で実装す ると、クラスファイルの数が飛躍的に増えるた め、クラスローディングが遅くなってしまいます • invokedynamic で、クラスを実行時に生成す れば、起動時間が抑えられる、かもしれません 35
  • 37. 匿名クラスとラムダ式の起動時間比較 • 5,000 個の匿名クラス/ラムダをインスタンス 化するプログラムを実行(各10回) 36 平均 2,041ms 2,375ms CPU: Core i3-2120T (2.6 GHz) OS: Arch Linux, カーネル: 3.9.9-1-ARCH JVM: JDK-8 build b99 (64-bit)
  • 38. 匿名クラス<ラムダ式 の考察 • 匿名クラスの実行時間増加要因 A) I/O B) jar の解凍 • ラムダ式の実行時間増加要因 C) ブートストラップメソッド呼び出し(それにともな う MethodHandle 作成など) D) バイトコード生成 37 A+B < C+D となった?
  • 39. (2) インスタンス生成戦略の選択 • LambdaMetaFactory は JVM が提供する API です。したがって、実行時に JVM に都合 のよい方法でインスタンスが生成できます • 可能な選択肢: – 1 つのラムダ式ごとに 1 つのクラスを生成(既述) – 外部の値を参照しないラムダ式であれば、シング ルトンインスタンスを戻す(後述) 38
  • 40. シングルトンインスタンス • 次のラムダ式は、外側の変数に依存していな いため、何度実行しても、同じ働きのインスタ ンスを戻します →この場合、シングルトンインスタンスを毎回使 い回せばいいはずです 39 Comparator<String> comparator() { return (x, y) -> x.length() - y.length(); }
  • 41. シングルトンインスタンス: 実行の流れ • ラムダの処理の本体が、外側の変数に依存し ていない場合、ラムダ式はシングルトンインス タンスを戻します 40 invoke dynamic <<ブートストラップ>> LambdaMetaFactory #metaFactory シングルトン インスタンス 2 回目以降の 実行 1. ラムダのクラスを生成 2. ラムダのシングルトンインスタンスを生成 3. シングルトンインスタンスを戻す MethodHandle を生成
  • 42. シングルトンインスタンス: 確認 41 $ cat >Lambda.java import java.util.*; public class Lambda { static Comparator<String> comparator() { return (x, y) -> x.length() - y.length(); } public static void main(String[] args) { System.out.println(comparator()); System.out.println(comparator()); System.out.println(comparator()); } } $ javac Lambda.java && java Lambda Lambda$$Lambda$1@84aee7 Lambda$$Lambda$1@84aee7 Lambda$$Lambda$1@84aee7
  • 43. その他の可能なインスタンス生成戦略 • 1 つの関数型インタフェースごとに 1 つのクラ スを生成。リフレクション経由で処理本体を呼 び出し • MethodHandle を関数型インタフェースに直 接ラップする機構を用意して、それを使う 42
  • 44. ラムダの直列化 43
  • 45. ラムダの直列化 • 関数型インタフェースが Serializable を拡張 している場合、ラムダのインスタンスは直列化 できる必要があります – 直列化(ラムダ→バイト列)、非直列化(バイト列→ ラムダ)した時、元のラムダと同じように機能する 必要がある • ラムダのクラスが実行時に生成される時、どう したら直列化・非直列化できるのでしょうか? → writeReplace / readResolve を使う 44
  • 46. 直列化の流れ 45 ラムダ インスタンス SerializedLambda バイト列 writeReplace インタフェース名、処理本体 のメソッド名など、ラムダを構 成する静的情報を保持 defaultWriteObject
  • 47. 非直列化の流れ 46 defaultReadObject ラムダ インスタンス SerializedLambdaバイト列 ★ ★: 静的情報を元にラムダを復元 SerializedLambda ラムダ式を含むクラス ラムダ readResolve $deserializeLambda$ <<create>> invokedynamic コンパイル時に生成
  • 48. 総括 47
  • 49. 総括 • ラムダ式は匿名クラスの単純な構文糖ではあ りません。 invokedynamic 命令を使って、クラ スを実行時に生成しています • これにより、 JVM がラムダのインスタンスの生 成方法を選べるので、実行時最適化の余地 が大きくなります 48
  • 50. 参考文献 49
  • 51. 参考文献 • Java SE 8 API Specification – http://download.java.net/jdk8/docs/api/overview-summary.html • JSR-335 – http://jcp.org/en/jsr/detail?id=335 • Brian Goetz “From Lambdas to Bytecode” – http://wiki.jvmlangsummit.com/images/1/1e/2011_Goetz_Lambd a.pdf • 宮川 拓「Lambda 式に invokedynamic を使うのかもしれな い話」 – http://d.hatena.ne.jp/miyakawa_taku/20120728/1343478485 50