ラムダと invokedynamic の蜜月

51
ラムダと invokedynamic の蜜月 宮川 / @miyakawa_taku 2013-07-22 JJUG ナイトセミナー

Upload: taku-miyakawa

Post on 15-Jan-2015

4.232 views

Category:

Documents


0 download

DESCRIPTION

 

TRANSCRIPT

Page 1: ラムダと invokedynamic の蜜月

ラムダと invokedynamic の蜜月

宮川 拓 / @miyakawa_taku

2013-07-22 JJUG ナイトセミナー

Page 2: ラムダと invokedynamic の蜜月

自己紹介

• 宮川 拓 (@miyakawa_taku) と申します

• SI 屋です

• JJUG 幹事です

• Kink という JVM 言語を開発しています

– https://bitbucket.org/kink/kink

1

Page 3: ラムダと invokedynamic の蜜月

要旨

• ラムダ式の実行は invokedynamic で実現されます。その理由と、実行の流れを見ます

–論点整理

–ラムダ式の実行 (1)

– invokedynamic の復習

–ラムダ式の実行 (2)

–なぜ invokedynamic?

–ラムダの直列化

2

※注記

この資料は、 JDK, JRE の「仕様」と「実装」を厳密に区別していません。

Page 4: ラムダと invokedynamic の蜜月

論点整理

3

Page 5: ラムダと invokedynamic の蜜月

静的構造

4

関数型インタフェース

ラムダのクラス

ラムダのインスタンス

instance-of

implements

Comparable

Comparable<String> c = (x, y) -> x.length() - y.length();

c

c のクラス

Page 6: ラムダと invokedynamic の蜜月

実行の流れ

5

Comparable<String> c = (x, y) -> x.length() - y.length();

Collections.sort(strings, c);

main Collections

ラムダ

sort compare / 処理の中身の実行

new / ラムダ式の実行

今回の主な論点

Page 7: ラムダと invokedynamic の蜜月

ラムダ式の実行 (1)

6

Page 8: ラムダと invokedynamic の蜜月

ラムダのクラスの実行時生成

• 匿名クラスがコンパイル時に生成されるのに対し、ラムダのクラスは実行時に生成されます。まずはそれを確かめます

7

関数型インタフェース

ラムダのクラス

ラムダのインスタンス

instance-of

implements

実行時に生成

Page 9: ラムダと invokedynamic の蜜月

匿名クラスをコンパイル

• 匿名クラスは、「外側のクラス名$連番」という名前で、コンパイラによって生成されます

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

Page 10: ラムダと invokedynamic の蜜月

ラムダをコンパイル

• 同等のラムダをコンパイルしても、対応するクラスファイルは生成されません

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

Page 11: ラムダと invokedynamic の蜜月

ラムダのクラスの生成タイミング

• ラムダのクラスが、コンパイル時には生成されないことが分かりました

• したがって、実行時のどこかのタイミングで生成されているはずです

10

ラムダを含む

ソース

クラス

ファイル

ラムダの

クラスの生成

ラムダの

インスタンス化

コンパイル (JDK) 実行 (JVM)

この時点では

ラムダのクラスは生成されない

どこかの

タイミング

Page 12: ラムダと invokedynamic の蜜月

ラムダのクラスの名前

• まずはラムダのクラスの名前を確認します

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

Page 13: ラムダと invokedynamic の蜜月

生成のタイミング

• 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

Page 14: ラムダと invokedynamic の蜜月

ラムダ式実行の過程

• ラムダ式を実行するタイミングで、ラムダのクラスが生成されていることが分かりました

• ではラムダ式は、バイトコードのレベルでは、どのような過程で実行されているのでしょうか?

13

Page 15: ラムダと invokedynamic の蜜月

ラムダ式のバイトコード

• ラムダ式を含むプログラムのクラスファイルを、 javap コマンドで逆アセンブルします

14

$ cat >Lambda.java

import java.util.function.*;

class Lambda {

IntUnaryOperator adder(int delta) {

return n -> n + delta;

}

}

$ javap -c -p Lambda.class

... (次ページ) ...

Page 16: ラムダと invokedynamic の蜜月

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

}

Page 17: ラムダと invokedynamic の蜜月

再度 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;

}

}

Page 18: ラムダと invokedynamic の蜜月

ここまでの整理

• 分かったこと

–ラムダのクラスはラムダ式実行の際に生成されます

–ラムダ式の実行は invokedynamic 命令です

• 推定できること

– invokedynamic 命令をきっかけとして、ラムダのクラスが生成され、またラムダのインスタンスが生成されるはずです

17

Page 19: ラムダと invokedynamic の蜜月

invokedynamic の復習

18

Page 20: ラムダと invokedynamic の蜜月

invokedynamic の復習

• ラムダ式実行の流れを追いかけるにあたり、まずは invokedynamic をおさらいします

19

Page 21: ラムダと invokedynamic の蜜月

invokedynamic とは

• 本来は、 JRuby など、 Java 以外の言語処理系のために、 Java SE 7 で追加されたメソッド呼び出し命令です

• invokevirtual, invokeinterface など、 Java SE

6 までのメソッド呼び出し命令と異なり、呼び出す処理が実行時に選択できます

20

Page 22: ラムダと invokedynamic の蜜月

Java のメソッド呼び出し手順

• どの呼び出し命令でも、手順は大体同じです

21

int result = receiver.doSomething(arg0, arg1);

receiver

arg0

receiver

arg1

arg0

receiver 戻り値

invokexxx

レシーバと引数をスタックに積む 呼び出し 結果も

スタックに

void

以外の場合

Page 23: ラムダと invokedynamic の蜜月

Java SE 6 までの呼び出し命令

• Java SE 6 までの呼び出し命令は、いずれも

Java 言語と密に結びついてます

– メソッドは再定義されない

–名前、引数の型、レシーバのクラスが決まれば、呼び出すべき処理が定まる

22

invokestatic static メソッドを呼び出す

invokespecial コンストラクタ、 private メソッド等を呼び出す

invokevirtual クラスに属するメソッドを呼び出す

invokeinterface インタフェースに属するメソッドを呼び出す

Page 24: ラムダと invokedynamic の蜜月

Java 以外の言語処理系の実装

―Java SE 6 以前

• Java 言語にない機構(メソッド再定義など)を実現するため、処理系が呼び出しに介入

→JVM による実行時最適化が効きづらい

23

array.join

invoke

virtual 処理系

size Func@42

join Func@123

検索

def join

...

invoke

virtual

関数テーブル

Page 25: ラムダと invokedynamic の蜜月

Java 以外の言語処理系の実装

―Java SE 7 以降

• invokedynamic を使って、処理系を介さずに、直接メソッドが呼び出せるようになりました

→JVM による実行時最適化が効きやすい!

24

array.join invokedynamic def join

...

Page 26: ラムダと invokedynamic の蜜月

invokedynamic の道具立て

• 呼び出し元 (CallSite) ごとに、ブートストラップメソッドで、呼び出し先の関数ポインタ (MethodHandle) を登録

25

Method

Handle

オブジェクト

CallSite

オブジェクト

ブートストラップ

メソッド <<create>>

初回呼び出しの前に

実行

対象の

処理

呼び出し元

<<create>>

呼び出し

Page 27: ラムダと invokedynamic の蜜月

ブートストラップメソッド

• static である必要がある

• ブートストラップメソッドの引数

– Lookup: MethodHandle のファクトリ

– String: 「メソッド名」だが、使わなくても可

– MethodType: invokedynamic の引数型と戻り値型

– 任意個数の定数

• ブートストラップメソッドの戻り値

– MethodHandle の初期値が紐付けられた CallSite

26

Page 28: ラムダと invokedynamic の蜜月

ブートストラップメソッドの例

• メソッドを呼び出した後、強制的に戻り値を

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 の動作は、ブートストラップメソッドを見れば見当が付きます

Page 29: ラムダと invokedynamic の蜜月

ラムダ式の実行 (2)

28

Page 30: ラムダと invokedynamic の蜜月

ラムダ式の invokedynamic

• 先ほど見たところでは、ラムダ式の実行は、 invokedynamic 命令の実行として実装されていました

→紐付けられているブートストラップメソッドを見れば、実際の動作がわかるはずです

29

Page 31: ラムダと invokedynamic の蜜月

ラムダ式の実行の流れ

• ラムダ式の実行の invokedynamic には、 java.lang.invoke.LambdaMetaFactory の metaFactory メソッドがブートストラップメソッドとして紐付いています

30

invoke

dynamic

<<ブートストラップ>>

LambdaMetaFactory

#metaFactory

ラムダの

インスタンス化

2 回目以降の

実行

1. ラムダのクラスを生成

2. ラムダをインスタンス化する

MethodHandle を生成

Page 32: ラムダと invokedynamic の蜜月

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 に紐付け

Page 33: ラムダと invokedynamic の蜜月

最終的に実行される処理

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

が生成

→ 結局、やってることは匿名クラスと(ほぼ)同じ!

Page 34: ラムダと invokedynamic の蜜月

なぜ invokedynamic?

33

Page 35: ラムダと invokedynamic の蜜月

なぜ invokedynamic?

• invokedynamic によるラムダ式の実行は、動作としては匿名クラスと似たようなものでした

• なぜ、わざわざ invokedynamic を使うのでしょうか?

→(1) クラスファイルが少なくなるおかげで、起動が速くなる、かもしれません

→(2) JVM が LambdaMetaFactory を独自に実装することで、最適なインスタンス生成の方法を選択できるようになります

34

Page 36: ラムダと invokedynamic の蜜月

(1) 起動時間

• Java SE 8 では、 Streams API の採用によって、プログラム中で全面的にラムダ式が利用されることが想定されています(実態はどうあれ!)

• その際、ラムダ式を匿名クラス方式で実装すると、クラスファイルの数が飛躍的に増えるため、クラスローディングが遅くなってしまいます

• invokedynamic で、クラスを実行時に生成すれば、起動時間が抑えられる、かもしれません

35

Page 37: ラムダと invokedynamic の蜜月

匿名クラスとラムダ式の起動時間比較

• 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)

Page 38: ラムダと invokedynamic の蜜月

匿名クラス<ラムダ式 の考察

• 匿名クラスの実行時間増加要因

A) I/O

B) jar の解凍

• ラムダ式の実行時間増加要因

C) ブートストラップメソッド呼び出し(それにともなう MethodHandle 作成など)

D) バイトコード生成

37

A+B < C+D となった?

Page 39: ラムダと invokedynamic の蜜月

(2) インスタンス生成戦略の選択

• LambdaMetaFactory は JVM が提供する

API です。したがって、実行時に JVM に都合のよい方法でインスタンスが生成できます

• 可能な選択肢:

– 1 つのラムダ式ごとに 1 つのクラスを生成(既述)

–外部の値を参照しないラムダ式であれば、シングルトンインスタンスを戻す(後述)

38

Page 40: ラムダと invokedynamic の蜜月

シングルトンインスタンス

• 次のラムダ式は、外側の変数に依存していないため、何度実行しても、同じ働きのインスタンスを戻します

→この場合、シングルトンインスタンスを毎回使い回せばいいはずです

39

Comparator<String> comparator() {

return (x, y) -> x.length() - y.length();

}

Page 41: ラムダと invokedynamic の蜜月

シングルトンインスタンス: 実行の流れ

• ラムダの処理の本体が、外側の変数に依存していない場合、ラムダ式はシングルトンインスタンスを戻します

40

invoke

dynamic

<<ブートストラップ>>

LambdaMetaFactory

#metaFactory

シングルトン

インスタンス

2 回目以降の

実行

1. ラムダのクラスを生成

2. ラムダのシングルトンインスタンスを生成

3. シングルトンインスタンスを戻す

MethodHandle を生成

Page 42: ラムダと invokedynamic の蜜月

シングルトンインスタンス: 確認

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

Page 43: ラムダと invokedynamic の蜜月

その他の可能なインスタンス生成戦略

• 1 つの関数型インタフェースごとに 1 つのクラスを生成。リフレクション経由で処理本体を呼び出し

• MethodHandle を関数型インタフェースに直接ラップする機構を用意して、それを使う

42

Page 44: ラムダと invokedynamic の蜜月

ラムダの直列化

43

Page 45: ラムダと invokedynamic の蜜月

ラムダの直列化

• 関数型インタフェースが Serializable を拡張している場合、ラムダのインスタンスは直列化できる必要があります

–直列化(ラムダ→バイト列)、非直列化(バイト列→

ラムダ)した時、元のラムダと同じように機能する必要がある

• ラムダのクラスが実行時に生成される時、どうしたら直列化・非直列化できるのでしょうか?

→ writeReplace / readResolve を使う

44

Page 46: ラムダと invokedynamic の蜜月

直列化の流れ

45

ラムダ

インスタンス SerializedLambda バイト列

writeReplace

インタフェース名、処理本体のメソッド名など、ラムダを構成する静的情報を保持

defaultWriteObject

Page 47: ラムダと invokedynamic の蜜月

非直列化の流れ

46

defaultReadObject

ラムダ

インスタンス SerializedLambda バイト列

★: 静的情報を元にラムダを復元

SerializedLambda ラムダ式を含むクラス

ラムダ

readResolve $deserializeLambda$

<<create>>

invokedynamic コンパイル時に生成

Page 48: ラムダと invokedynamic の蜜月

総括

47

Page 49: ラムダと invokedynamic の蜜月

総括

• ラムダ式は匿名クラスの単純な構文糖ではありません。 invokedynamic 命令を使って、クラスを実行時に生成しています

• これにより、 JVM がラムダのインスタンスの生成方法を選べるので、実行時最適化の余地が大きくなります

48

Page 50: ラムダと invokedynamic の蜜月

参考文献

49