- Contents -
Javaのリフレクションは、メソッドコールに時間がかかる。
MethodHandlerを使ったら、リフレクションより早く実行出来るのか速度計測してみました。
Method::invoke
と MethodHandle::invokeExact
の速度比較になります。
結論を先に書くと、両者に違いはそんなになかったです!
むしろ、両者を比べるとリフレクションのほうが若干速い。
ただ、static final MethodHandleで宣言したフィールドを使ってコールすると
ダイレクトコールに近い速度が出るようです!
MethodHandleのコール方法
こんなクラスがあったとして、
fooメソッドをMethodHandleでコールしたい場合は下記のようになります。
class Sample {
String foo(int num, String str) {
return str + num;
}
}
void sampleMethodHandle() throws Throwable {
// ルックアップ作成
MethodHandles.Lookup lookup = MethodHandles.lookup();
// メソッドのタイプ作成
// 返り値がStringで、int, String を引数にとるメソッドという事
MethodType mt = MethodType.methodType(String.class, int.class, String.class);
// メソッドハンドルの取得
MethodHandle mh = lookup.findVirtual(Sample.class, "foo", mt);
// コールするメソッドのインスタンス
var o = new Sample();
// メソッドコール
String ret = (String) mh.invokeExact(o, 123, "abc");
}
MethodHandleのインスタンスを保持しておけば、
invokeExactをコールすることで目的のメソッドをコールすることができます。
MethodHandle::invoke
, MethodHandle::invokeWithArguments
メソッドもありますが、
引数の型チェックと変換処理がはいるようになる為、reflectionよりも遅くなる可能性があります。
メソッドの型がはっきりしている場合は、invokeExact
を使うほうが良いでしょう。
Method -> MethodHandle変換
Lookup::unreflect
メソッドを使えば、
リフレクションのMethodクラスから、MethodHandleに変換することも可能です。
Method m = Sample.class.getDeclaredMethod("foo", int.class, String.class);
// MethodHandleに変換
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.unreflect(m);
Reflectionのコール方法
前述と同様に、Sampleクラスのfooメソッドをリフレクションを使ってコールする例です。
void sampleReflection()
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// リフレクションMethodの取得
// 引数に、int, Stringを取るメソッド (返り値の型は指定しない)
Method m = Sample.class.getDeclaredMethod("foo", int.class, String.class);
// コールするメソッドのインスタンス
var o = new Sample();
// メソッドコール
String ret = (String) m.invoke(o, 123, "abc");
}
getDeclaredMethod
を使って、Methodを取得します。
速度計測
直接コール (DirectCall)
まず、ReflectionもMethondHandleも使わずに
直接メソッドコールした場合の速度を計測。
void benchmarkDirectMethod() {
var o = new Sample();
// 計測開始
long startTime = System.currentTimeMillis();
// 100万回コール
for (int i = 0; i < 1_000_000; i++) {
String ret = o.foo(123, "abc");
}
// 計測終了
long endTime = System.currentTimeMillis();
System.out.println("benchDirectMethod: " + (endTime - startTime) + " ms");
}
Reflectionコール
続いて、Reflectionでのコール。
Method::invoke
の部分だけで測定しています。
void benchmarkReflection()
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
var o = new Sample();
Method m = Sample.class.getDeclaredMethod("foo", int.class, String.class);
// 計測開始
long startTime = System.currentTimeMillis();
// 100万回コール
for (int i = 0; i < 1_000_000; i++) {
String ret = (String) m.invoke(o, 123, "abc");
}
// 計測終了
long endTime = System.currentTimeMillis();
System.out.println("benchmarkReflection: " + (endTime - startTime) + " ms");
}
MethodHandleコール
最後にMethodHandleを使ったコール。
こちらも、MethodHandle::invokeExact
の部分だけを測定します。
void benchmarkMethodHandle()
throws Throwable {
var o = new Sample();
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class, int.class, String.class);
MethodHandle mh = lookup.findVirtual(Sample.class, "foo", mt);
// 計測開始
long startTime = System.currentTimeMillis();
// 100万回コール
for (int i = 0; i < 1_000_000; i++) {
String ret = (String) mh.invokeExact(o, 123, "abc");
}
// 計測終了
long endTime = System.currentTimeMillis();
System.out.println("benchmarkMethodHandle: " + (endTime - startTime) + " ms");
}
測定結果
それぞれのベンチマークで
100万回コールを15回ずつ実行したらこんな感じになりました。
num | Direct (Sample::foo) | Reflection (Method::invoke) | MethodHandle (MethodHandle::invokeExact) |
---|---|---|---|
1 | 135 ms | 223 ms | 142 ms |
2 | 36 ms | 134 ms | 69 ms |
3 | 24 ms | 55 ms | 80 ms |
4 | 25 ms | 28 ms | 67 ms |
5 | 23 ms | 33 ms | 49 ms |
6 | 22 ms | 34 ms | 41 ms |
7 | 23 ms | 33 ms | 37 ms |
8 | 7 ms | 32 ms | 35 ms |
9 | 5 ms | 30 ms | 35 ms |
10 | 5 ms | 30 ms | 33 ms |
11 | 4 ms | 30 ms | 49 ms |
12 | 4 ms | 33 ms | 44 ms |
13 | 4 ms | 31 ms | 39 ms |
14 | 6 ms | 37 ms | 47 ms |
15 | 4 ms | 31 ms | 43 ms |
MethodHandleより、Reflectionのほうが若干速い気が・・。
ダイレクトコールが後半めちゃ早くなってるのは、
JVMの最適化(ネイティブコートに変換)が行われる為だと思います。
このあたりの挙動はJVMの実装によって変わる気がする・・。
ちなみにAdoptOpenJDK のJava11を使って測定しています。
openjdk 11.0.4 2019-07-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.4+11)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.4+11, mixed mode)
static final MethodHandle フィールドを使うと早くなる?
どうやら、static final
で宣言されたメンバー変数は特別(Trusted)なようで、
static final MethodHandle _mh = ...
のようにしておいたMethodHandleであれば、
高速に動作するようです。
(理由までは調べきれなかった・・)
// static final で宣言
static final MethodHandle _mh = getMethodHandle();
static MethodHandle getMethodHandle() {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(String.class, int.class, String.class);
try {
return lookup.findVirtual(Sample.class, "foo", mt);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
ベンチマークの際は、static final
で宣言したMethodHandleを使ってコールします。
途中でローカル変数などに代入するとダメになります。
private static void benchmarkStaticMethodHandle()
throws Throwable {
var c = new Sample();
// 計測開始
long startTime = System.currentTimeMillis();
// 100万回コール
for (int i = 0; i < 1_000_000; i++) {
// static final で宣言された _mh を使ってコールする
String ret = (String) _mh.invokeExact(c, 123, "abc");
}
// 計測終了
long endTime = System.currentTimeMillis();
System.out.println("benchmarkStaticMethodHandle: " + (endTime - startTime) + " ms");
}
測定結果
測定結果がこちら、ダイレクトコールと変わらん速度で動作するようです。
num | static final MethodHandle |
---|---|
1 | 137 ms |
2 | 38 ms |
3 | 27 ms |
4 | 25 ms |
5 | 25 ms |
6 | 25 ms |
7 | 24 ms |
8 | 8 ms |
9 | 6 ms |
10 | 4 ms |
11 | 6 ms |
12 | 6 ms |
13 | 6 ms |
14 | 6 ms |
15 | 4 ms |
まとめ
-
Method::invoke
とMethodHandle::invokeExact
を比較すると、
リフレクションとほぼ同等か、少しMethodHandle::invokeExact
のほうが遅い。 -
static final MethodHandle
フィールドを使うと高速に動作する。
ちなみに、今回は MethodHandleをstatic final
にしていますが、
代わりにCallSiteを使っても同様に早くする事ができました。
あと、static final
について
static final Map<String, MethodHandle>
や
static final MethodHandle[]
でもいけるのかやってみましたが、
残念ながらこちらは速くなりませんでした・・。
1フィールドに1メソッドでしか宣言できないようです。