【Java】LambdaMetafactoryでリフレクションによる関数呼び出しを直接呼び出しレベルまで高速化する

この記事は[MicroAd \(マイクロアド\) Advent Calendar 2020](https://qiita.com/advent-calendar/2020/microad)の16日目の記事です。

# 前書き
`Java`のリフレクションによる関数呼び出しは直接呼び出しに比べて低速です。
例えば以下の記事中の検証では、5引数のコンストラクタを呼び出す場合、リフレクションによる`Constructor`呼び出しは直接呼び出しの1/5程度のスコアとなっています。

- [【Kotlin】KFunctionを高速に呼び出す（前編） \- Qiita](https://qiita.com/wrongwrong/items/f7b15d54956191f471d1#%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E9%80%9F%E5%BA%A6%E3%81%AE%E7%B0%A1%E5%8D%98%E3%81%AA%E6%AF%94%E8%BC%83)

この記事では、`Constructor`/`Method`から`LambdaMetafactory`を用いて`CallSite`を生成し、それを呼び出す形で呼び出しを高速化していきます。
環境は`Java8`です。

勉強しながら書いているような状況なので、間違った記述が有るかもしれません。
ご指摘いただければ幸いです。

なお、プロジェクト全体は`GitHub`に上げてあります。
後述するベンチマークは`./gradlew jmh`で起動できます。

- [k163377/LambdaMetafactorySample: Sample of LambdaMetafactory\.](https://github.com/k163377/LambdaMetafactorySample)

# LambdaMetafactoryの使い方
説明のため、コンストラクタとファクトリーメソッドを定義した以下のクラスを用います。

```java
public class SampleClass {
    private final int arg;

    public SampleClass(int arg) {
        this.arg = arg;
    }

    public static SampleClass factory(int arg) {
        return new SampleClass(arg);
    }

    public static SampleClass sum(int arg1, int arg2, int arg3, int arg4, int arg5) {
        return new SampleClass(arg1 + arg2 + arg3 + arg4 + arg5);
    }

    public int getArg() {
        return arg;
    }
}
```

## 1引数のstaticメソッド/getterに適用する例
以下は`LambdaMetafactory`を用いて引数1つの`Method`に対して高速に呼び出せる`Function`を返すコードの例です。
引数をコンストラクタに、`lookup.unreflect`を`lookup.unreflectConstructor`に変更すればコンストラクタでも同様に使うことができます。

```java
import java.lang.invoke.*;
import java.lang.reflect.Method;
import java.util.function.Function;

public class LambdaMetaFactoryWrapper {
    public static <T, R> Function<T, R> toOptimizedFunction(Method method) throws Throwable {
        // lookupはセキュリティ上の懸念も有るため使い捨てる or publicLookup()を使うのが良さそう
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        // Constructorの場合はlookup.unreflectConstructorを用いる
        MethodHandle methodHandle = lookup.unreflect(method);

        CallSite callSite = LambdaMetafactory.metafactory(
                lookup,
                // 下で指定しているインターフェースの呼び出しインターフェースの名前
                "apply",
                // site.target.invokeExact()した時に出てくるインターフェースのクラス
                MethodType.methodType(Function.class),
                // 引数情報、generic()することでintとIntegerのような指定が齟齬を起こさなくなる
                methodHandle.type().generic(),
                // 呼ばれる関数のMethodHandle
                methodHandle,
                // 引数情報
                methodHandle.type()
        );

        // uncheck castは必須
        @SuppressWarnings("unchecked")
        Function<T, R> function = (Function<T, R>) callSite.getTarget().invokeExact();

        return function;
    }
}
```

これは、例えば引数1つの`static`関数や、実行のためインスタンスを引数に取る`getter`などに適用できます。

```java:使用例
import org.junit.jupiter.api.Test;

import java.lang.reflect.Method;
import java.util.function.Function;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class LambdaMetaFactoryWrapperTest {
    // static関数に適用した例
    @Test
    void methodTest() throws Throwable {
        Method method = SampleClass.class.getDeclaredMethod("factory", int.class);

        Function<Integer, SampleClass> optimizedFunction = LambdaMetaFactoryWrapper.toOptimizedFunction(method);

        assertEquals(new SampleClass(1).getArg(), optimizedFunction.apply(1).getArg());
    }

    // getterに適用した例
    @Test
    void getterTest() throws Throwable {
        Method method = SampleClass.class.getDeclaredMethod("getArg");

        Function<SampleClass, Integer> optimizedFunction = LambdaMetaFactoryWrapper.toOptimizedFunction(method);

        SampleClass sampleClass = new SampleClass(1);
        assertEquals(sampleClass.getArg(), optimizedFunction.apply(sampleClass));
    }
}
```

以下、これらのコードを用いて説明をしていきます。

### MethodHandles.Lookupの取得について
`MethodHandles.Lookup`は、今回説明する範囲では`private`や`package private`といったアクセシビリティの管理のために用いられます。

例えば`SampleClass`内の関数が`private`になっている場合、`SampleClass`内で`MethodHandles.lookup()`して取得した`MethodHandles.Lookup`を用いなければ、この方法で関数を実行することはできません。
前後で`Method.setAccessible`してもダメでした。
`Java 9`以降であれば回避方法も有りそうに見えましたが、`Java8`では利用できない`API`のようでした。

サンプルコード内のコメントで「`lookupはセキュリティ上の懸念も有るため使い捨てる or publicLookup()を使うのが良さそう`」と補足しましたが、`MethodHandles.Lookup`はコンテキスト内の情報を保持するため、フィールドとして保持するのはあまりよろしくないでしょう。
仮に保持するにしても`MethodHandles.publicLookup()`で`public`な内容のみ取っておく程度が良いと思います。

- [MethodHandles\.Lookup \(Java Platform SE 8 \)](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/invoke/MethodHandles.Lookup.html)

### LambdaMetafactoryを用いたCallSiteの生成について
`CallSite`は`Lambda`のようなもので、今回の高速化において呼び出される実体です。
`LambdaMetafactory`から`CallSite`を生成する関数としては`metafactory`と`altMetafactory`が有りますが、`metafactory`を用いた生成の方が呼び出しが高速[^kousoku]なため、今回は`metafactory`を用いる形で解説します。

[^kousoku]: [Performance profiling ways of invoking a method dynamically \- Development \- Image\.sc Forum](https://forum.image.sc/t/performance-profiling-ways-of-invoking-a-method-dynamically/27471)より。

- [LambdaMetafactory \(Java Platform SE 8 \)](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/invoke/LambdaMetafactory.html)

#### "apply"と生成するインターフェースについて
サンプルコードでは、以下のように`"apply"`という文字列を引数に指定しています。
これはその下で指定している`Function`の呼び出し関数名です。

これが例えば`ToIntFunction`などであれば`"applyAsInt"`という文字列を渡すことになります。

```java:サンプルコードから抜粋
                // 下で指定しているインターフェースの呼び出しインターフェースの名前
                "apply",
                // site.target.invokeExact()した時に出てくるインターフェースのクラス
                MethodType.methodType(Function.class),
```

今回は生成するインターフェースとして`Function`を指定していますが、これは自分で作成したインターフェースなどでも大丈夫です。

#### MethodTypeの指定について
サンプルコードでは、`LambdaMetafactory.metafactory`の第4・第6引数に`MethodType`を渡しています。
これは関数の引数や戻り値の情報です。
第4引数に`generic()`しているのは、これをやらないと例えば`int`と`Integer`のような場合に代入できないことが有るためです。

```java:サンプルコードから抜粋
                // 引数情報、generic()することでintとIntegerのような指定が齟齬を起こさなくなる
                methodHandle.type().generic(),
                // 呼ばれる関数のMethodHandle
                methodHandle,
                // 引数情報
                methodHandle.type()
```

第4引数にのみ`generic()`して第6引数にしない理由は良く分かっていません。
[見つけたコード](https://www.codota.com/code/java/methods/java.lang.invoke.MethodType/generic)が大体こうしていたのでそれをまねています。
幾つか試行錯誤した限りではこの形での指定が良さそうでした。

また、一時変数を使わずに2つの引数でそれぞれ`methodHandle`から`type()`しているのは、実装を追った感じ`generic()`に副作用が有るように見えたためです。

- [MethodType \(Java Platform SE 8 \)](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/invoke/MethodType.html)

## 複数引数を要求する場合
次に、`SampleClass.sum`を題材に、複数引数を要求する場合について説明します。

`SampleClass.sum`は5引数を要求しますが、用意されている関数型インターフェースは取れて2引数までです。
そのため、ここでは5引数入力可能なインターフェースを自分で用意する形で行います。
なお、`FunctionalInterface`注釈は無くても動きます。

```java
public interface Function5<P1, P2, P3, P4, P5, R> {
    R invoke(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5);
}
```

生成方法や利用方法は先ほど説明した内容とほぼ同じです。

```java
import java.lang.invoke.*;
import java.lang.reflect.Method;
import java.util.function.Function;

public class LambdaMetaFactoryWrapper {

    /* toOptimizedFunction関数、既に説明したため省略 */

    public static <P1, P2, P3, P4, P5, R> Function5<P1, P2, P3, P4, P5, R> toOptimizedFunction5(
            Method method) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle methodHandle = lookup.unreflect(method);

        CallSite callSite = LambdaMetafactory.metafactory(
                lookup,
                "invoke", // Function5はinvokeで呼び出すため、引数も"invoke"に変更している
                MethodType.methodType(Function5.class),
                methodHandle.type().generic(),
                methodHandle,
                methodHandle.type()
        );

        //noinspection unchecked
        return (Function5<P1, P2, P3, P4, P5, R>) callSite.getTarget().invokeExact();
    }
}
```

```java:使用例
import org.junit.jupiter.api.Test;

import java.lang.reflect.Method;
import java.util.function.Function;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class LambdaMetaFactoryWrapperTest {

    /* 既に説明しているため省略 */

    // 5引数のstatic関数に適用した例
    @Test
    void function5Test() throws Throwable {
        Method method =
                SampleClass.class.getDeclaredMethod("sum", int.class, int.class, int.class, int.class, int.class);

        Function5<Integer, Integer, Integer, Integer, Integer, SampleClass> optimizedFunction =
                LambdaMetaFactoryWrapper.toOptimizedFunction5(method);

        assertEquals(15, optimizedFunction.invoke(1, 2, 3, 4, 5).getArg());
    }
}
```

# ベンチマーク
最後に、5引数の`static`メソッド（`SampleClass.sum`）について、直接呼び出し、`Method`呼び出し、`LambdaMetafactory`による高速呼び出しの3種類を`JMH`によるベンチマークで比較します。

ベンチマークには`JMH Gradle Plugin`(`me.champeau.gradle.jmh`)を利用します。
オプションは以下の通りです。

```kotlin:build.gradle.kts（抜粋）
jmh {
    failOnError = true
    isIncludeTests = false

    resultFormat = "CSV"
}
```

ベンチマークは以下のコードで行います。

```java
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

@State(Scope.Benchmark)
public class OptimizedFunction5Benchmark {
    private Method method;
    private Function5<Integer, Integer, Integer, Integer, Integer, SampleClass> function5;

    public OptimizedFunction5Benchmark() {
        try {
            method =
                    SampleClass.class.getDeclaredMethod("sum", int.class, int.class, int.class, int.class, int.class);
            function5 = LambdaMetaFactoryWrapper.toOptimizedFunction5(method);
        } catch (Throwable t) {
            System.out.println(t);
            method = null;
            function5 = null;
        }
    }

    @Benchmark
    public SampleClass directCall() {
        return SampleClass.sum(1, 2, 3, 4, 5);
    }

    @Benchmark
    public SampleClass methodCall() throws InvocationTargetException, IllegalAccessException {
        return (SampleClass) method.invoke(null, 1, 2, 3, 4, 5);
    }

    @Benchmark
    public SampleClass optimizedFunction5Call() {
        return function5.invoke(1, 2, 3, 4, 5);
    }
}
```

なお、冒頭で述べた通りこれらの内容は`GitHub`にアップロード済みで、ベンチマークは`./gradlew jmh`で起動できます。

## 結果と考察
以下は手元の`Ryzen7 3700X`の`Windows`環境での計測結果で、スコアは高いほど良いです。

結果としては、直接呼び出しが最も良く、紹介した手法はそれに次ぐスコアとなり、`Method`を呼び出すよりも圧倒的に高速なことが分かります。

```
Benchmark                                            Mode  Cnt          Score        Error  Units
OptimizedFunction5Benchmark.directCall              thrpt   25  202126189.206 ± 286352.530  ops/s
OptimizedFunction5Benchmark.methodCall              thrpt   25   48196055.765 ± 176281.150  ops/s
OptimizedFunction5Benchmark.optimizedFunction5Call  thrpt   25  192184568.216 ± 345476.531  ops/s
```

# 終わりに
今回は`Constructor`/`Method`から`LambdaMetafactory`を用いて`CallSite`を生成し、それを呼び出す形で呼び出しを高速化する方法を紹介しました。
自分の知識が足りないことも有って「何故上手く動かないのか」と詰まってばかりでしたが、どうにかこうにか動きそうな形にできて良かったです。

とは言え自分の知識不足から把握できない部分が多く、「これ`Constructor`/`Method`で良くないか……？」と感じる点も多く有り、「使いこなせる気がしない」というのが感想です。
圧倒的に高速なことは確かなので、[自作ライブラリ](https://github.com/ProjectMapK/FastKFunction)に組み込めたらいいなとは思いますが……。

何にせよもう少し研究を続けてみます。

# 参考にさせて頂いた記事
- [OptaPlanner \- Java Reflection, but much faster](https://www.optaplanner.org/blog/2018/01/09/JavaReflectionButMuchFaster.html)
- [Performance profiling ways of invoking a method dynamically \- Development \- Image\.sc Forum](https://forum.image.sc/t/performance-profiling-ways-of-invoking-a-method-dynamically/27471)
- [Think Twice Before Using Reflection – CUBA Platform](https://www.cuba-platform.com/blog/think-twice-before-using-reflection/)
- [Hervian/lambda\-factory: A fast alternative to Java Reflection API's method invocation](https://github.com/Hervian/lambda-factory)