MethodHandleを使ったら、リフレクションより早くメソッドを実行できる?

Javaのリフレクションは、メソッドコールに時間がかかる。
MethodHandlerを使ったら、リフレクションより早く実行出来るのか速度計測してみました。

Method::invokeMethodHandle::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::invokeMethodHandle::invokeExact を比較すると、
    リフレクションとほぼ同等か、少しMethodHandle::invokeExactのほうが遅い。

  • static final MethodHandle フィールドを使うと高速に動作する。
    ちなみに、今回は MethodHandleを static final にしていますが、
    代わりにCallSiteを使っても同様に早くする事ができました。

あと、static final について
static final Map<String, MethodHandle>
static final MethodHandle[] でもいけるのかやってみましたが、

残念ながらこちらは速くなりませんでした・・。
1フィールドに1メソッドでしか宣言できないようです。

コメントを残す