ラムダと invokedynamic の蜜月
DESCRIPTION
TRANSCRIPT
ラムダと invokedynamic の蜜月
宮川 拓 / @miyakawa_taku
2013-07-22 JJUG ナイトセミナー
自己紹介
• 宮川 拓 (@miyakawa_taku) と申します
• SI 屋です
• JJUG 幹事です
• Kink という JVM 言語を開発しています
– https://bitbucket.org/kink/kink
1
要旨
• ラムダ式の実行は invokedynamic で実現されます。その理由と、実行の流れを見ます
–論点整理
–ラムダ式の実行 (1)
– invokedynamic の復習
–ラムダ式の実行 (2)
–なぜ invokedynamic?
–ラムダの直列化
2
※注記
この資料は、 JDK, JRE の「仕様」と「実装」を厳密に区別していません。
論点整理
3
静的構造
4
関数型インタフェース
ラムダのクラス
ラムダのインスタンス
instance-of
implements
Comparable
Comparable<String> c = (x, y) -> x.length() - y.length();
c
c のクラス
実行の流れ
5
Comparable<String> c = (x, y) -> x.length() - y.length();
Collections.sort(strings, c);
main Collections
ラムダ
sort compare / 処理の中身の実行
new / ラムダ式の実行
今回の主な論点
ラムダ式の実行 (1)
6
ラムダのクラスの実行時生成
• 匿名クラスがコンパイル時に生成されるのに対し、ラムダのクラスは実行時に生成されます。まずはそれを確かめます
7
関数型インタフェース
ラムダのクラス
ラムダのインスタンス
instance-of
implements
実行時に生成
匿名クラスをコンパイル
• 匿名クラスは、「外側のクラス名$連番」という名前で、コンパイラによって生成されます
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
ラムダをコンパイル
• 同等のラムダをコンパイルしても、対応するクラスファイルは生成されません
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
ラムダのクラスの生成タイミング
• ラムダのクラスが、コンパイル時には生成されないことが分かりました
• したがって、実行時のどこかのタイミングで生成されているはずです
10
ラムダを含む
ソース
クラス
ファイル
ラムダの
クラスの生成
ラムダの
インスタンス化
コンパイル (JDK) 実行 (JVM)
この時点では
ラムダのクラスは生成されない
どこかの
タイミング
ラムダのクラスの名前
• まずはラムダのクラスの名前を確認します
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
生成のタイミング
• 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
ラムダ式実行の過程
• ラムダ式を実行するタイミングで、ラムダのクラスが生成されていることが分かりました
• ではラムダ式は、バイトコードのレベルでは、どのような過程で実行されているのでしょうか?
13
ラムダ式のバイトコード
• ラムダ式を含むプログラムのクラスファイルを、 javap コマンドで逆アセンブルします
14
$ cat >Lambda.java
import java.util.function.*;
class Lambda {
IntUnaryOperator adder(int delta) {
return n -> n + delta;
}
}
$ javap -c -p Lambda.class
... (次ページ) ...
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
}
再度 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;
}
}
ここまでの整理
• 分かったこと
–ラムダのクラスはラムダ式実行の際に生成されます
–ラムダ式の実行は invokedynamic 命令です
• 推定できること
– invokedynamic 命令をきっかけとして、ラムダのクラスが生成され、またラムダのインスタンスが生成されるはずです
17
invokedynamic の復習
18
invokedynamic の復習
• ラムダ式実行の流れを追いかけるにあたり、まずは invokedynamic をおさらいします
19
invokedynamic とは
• 本来は、 JRuby など、 Java 以外の言語処理系のために、 Java SE 7 で追加されたメソッド呼び出し命令です
• invokevirtual, invokeinterface など、 Java SE
6 までのメソッド呼び出し命令と異なり、呼び出す処理が実行時に選択できます
20
Java のメソッド呼び出し手順
• どの呼び出し命令でも、手順は大体同じです
21
int result = receiver.doSomething(arg0, arg1);
receiver
arg0
receiver
arg1
arg0
receiver 戻り値
invokexxx
レシーバと引数をスタックに積む 呼び出し 結果も
スタックに
void
以外の場合
Java SE 6 までの呼び出し命令
• Java SE 6 までの呼び出し命令は、いずれも
Java 言語と密に結びついてます
– メソッドは再定義されない
–名前、引数の型、レシーバのクラスが決まれば、呼び出すべき処理が定まる
22
invokestatic static メソッドを呼び出す
invokespecial コンストラクタ、 private メソッド等を呼び出す
invokevirtual クラスに属するメソッドを呼び出す
invokeinterface インタフェースに属するメソッドを呼び出す
Java 以外の言語処理系の実装
―Java SE 6 以前
• Java 言語にない機構(メソッド再定義など)を実現するため、処理系が呼び出しに介入
→JVM による実行時最適化が効きづらい
23
array.join
invoke
virtual 処理系
size Func@42
join Func@123
検索
def join
...
invoke
virtual
関数テーブル
Java 以外の言語処理系の実装
―Java SE 7 以降
• invokedynamic を使って、処理系を介さずに、直接メソッドが呼び出せるようになりました
→JVM による実行時最適化が効きやすい!
24
array.join invokedynamic def join
...
invokedynamic の道具立て
• 呼び出し元 (CallSite) ごとに、ブートストラップメソッドで、呼び出し先の関数ポインタ (MethodHandle) を登録
25
Method
Handle
オブジェクト
CallSite
オブジェクト
ブートストラップ
メソッド <<create>>
初回呼び出しの前に
実行
対象の
処理
呼び出し元
<<create>>
呼び出し
ブートストラップメソッド
• static である必要がある
• ブートストラップメソッドの引数
– Lookup: MethodHandle のファクトリ
– String: 「メソッド名」だが、使わなくても可
– MethodType: invokedynamic の引数型と戻り値型
– 任意個数の定数
• ブートストラップメソッドの戻り値
– MethodHandle の初期値が紐付けられた CallSite
26
ブートストラップメソッドの例
• メソッドを呼び出した後、強制的に戻り値を
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 の動作は、ブートストラップメソッドを見れば見当が付きます
ラムダ式の実行 (2)
28
ラムダ式の invokedynamic
• 先ほど見たところでは、ラムダ式の実行は、 invokedynamic 命令の実行として実装されていました
→紐付けられているブートストラップメソッドを見れば、実際の動作がわかるはずです
29
ラムダ式の実行の流れ
• ラムダ式の実行の invokedynamic には、 java.lang.invoke.LambdaMetaFactory の metaFactory メソッドがブートストラップメソッドとして紐付いています
30
invoke
dynamic
<<ブートストラップ>>
LambdaMetaFactory
#metaFactory
ラムダの
インスタンス化
2 回目以降の
実行
1. ラムダのクラスを生成
2. ラムダをインスタンス化する
MethodHandle を生成
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 に紐付け
最終的に実行される処理
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
が生成
→ 結局、やってることは匿名クラスと(ほぼ)同じ!
なぜ invokedynamic?
33
なぜ invokedynamic?
• invokedynamic によるラムダ式の実行は、動作としては匿名クラスと似たようなものでした
• なぜ、わざわざ invokedynamic を使うのでしょうか?
→(1) クラスファイルが少なくなるおかげで、起動が速くなる、かもしれません
→(2) JVM が LambdaMetaFactory を独自に実装することで、最適なインスタンス生成の方法を選択できるようになります
34
(1) 起動時間
• Java SE 8 では、 Streams API の採用によって、プログラム中で全面的にラムダ式が利用されることが想定されています(実態はどうあれ!)
• その際、ラムダ式を匿名クラス方式で実装すると、クラスファイルの数が飛躍的に増えるため、クラスローディングが遅くなってしまいます
• invokedynamic で、クラスを実行時に生成すれば、起動時間が抑えられる、かもしれません
35
匿名クラスとラムダ式の起動時間比較
• 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)
匿名クラス<ラムダ式 の考察
• 匿名クラスの実行時間増加要因
A) I/O
B) jar の解凍
• ラムダ式の実行時間増加要因
C) ブートストラップメソッド呼び出し(それにともなう MethodHandle 作成など)
D) バイトコード生成
37
A+B < C+D となった?
(2) インスタンス生成戦略の選択
• LambdaMetaFactory は JVM が提供する
API です。したがって、実行時に JVM に都合のよい方法でインスタンスが生成できます
• 可能な選択肢:
– 1 つのラムダ式ごとに 1 つのクラスを生成(既述)
–外部の値を参照しないラムダ式であれば、シングルトンインスタンスを戻す(後述)
38
シングルトンインスタンス
• 次のラムダ式は、外側の変数に依存していないため、何度実行しても、同じ働きのインスタンスを戻します
→この場合、シングルトンインスタンスを毎回使い回せばいいはずです
39
Comparator<String> comparator() {
return (x, y) -> x.length() - y.length();
}
シングルトンインスタンス: 実行の流れ
• ラムダの処理の本体が、外側の変数に依存していない場合、ラムダ式はシングルトンインスタンスを戻します
40
invoke
dynamic
<<ブートストラップ>>
LambdaMetaFactory
#metaFactory
シングルトン
インスタンス
2 回目以降の
実行
1. ラムダのクラスを生成
2. ラムダのシングルトンインスタンスを生成
3. シングルトンインスタンスを戻す
MethodHandle を生成
シングルトンインスタンス: 確認
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
その他の可能なインスタンス生成戦略
• 1 つの関数型インタフェースごとに 1 つのクラスを生成。リフレクション経由で処理本体を呼び出し
• MethodHandle を関数型インタフェースに直接ラップする機構を用意して、それを使う
42
ラムダの直列化
43
ラムダの直列化
• 関数型インタフェースが Serializable を拡張している場合、ラムダのインスタンスは直列化できる必要があります
–直列化(ラムダ→バイト列)、非直列化(バイト列→
ラムダ)した時、元のラムダと同じように機能する必要がある
• ラムダのクラスが実行時に生成される時、どうしたら直列化・非直列化できるのでしょうか?
→ writeReplace / readResolve を使う
44
直列化の流れ
45
ラムダ
インスタンス SerializedLambda バイト列
writeReplace
インタフェース名、処理本体のメソッド名など、ラムダを構成する静的情報を保持
defaultWriteObject
非直列化の流れ
46
defaultReadObject
ラムダ
インスタンス SerializedLambda バイト列
★
★: 静的情報を元にラムダを復元
SerializedLambda ラムダ式を含むクラス
ラムダ
readResolve $deserializeLambda$
<<create>>
invokedynamic コンパイル時に生成
総括
47
総括
• ラムダ式は匿名クラスの単純な構文糖ではありません。 invokedynamic 命令を使って、クラスを実行時に生成しています
• これにより、 JVM がラムダのインスタンスの生成方法を選べるので、実行時最適化の余地が大きくなります
48
参考文献
49
参考文献
• 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