この記事はMicroAd (マイクロアド) Advent Calendar 2020の16日目の記事です。
前書き
Java
のリフレクションによる関数呼び出しは直接呼び出しに比べて低速です。
例えば以下の記事中の検証では、5引数のコンストラクタを呼び出す場合、リフレクションによるConstructor
呼び出しは直接呼び出しの1/5程度のスコアとなっています。
この記事では、Constructor
/Method
からLambdaMetafactory
を用いてCallSite
を生成し、それを呼び出す形で呼び出しを高速化していきます。
環境はJava8
です。
勉強しながら書いているような状況なので、間違った記述が有るかもしれません。
ご指摘いただければ幸いです。
なお、プロジェクト全体はGitHub
に上げてあります。
後述するベンチマークは./gradlew jmh
で起動できます。
LambdaMetafactoryの使い方
説明のため、コンストラクタとファクトリーメソッドを定義した以下のクラスを用います。
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
に変更すればコンストラクタでも同様に使うことができます。
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
などに適用できます。
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
な内容のみ取っておく程度が良いと思います。
LambdaMetafactoryを用いたCallSiteの生成について
CallSite
はLambda
のようなもので、今回の高速化において呼び出される実体です。
LambdaMetafactory
からCallSite
を生成する関数としてはmetafactory
とaltMetafactory
が有りますが、metafactory
を用いた生成の方が呼び出しが高速1なため、今回はmetafactory
を用いる形で解説します。
"apply"と生成するインターフェースについて
サンプルコードでは、以下のように"apply"
という文字列を引数に指定しています。
これはその下で指定しているFunction
の呼び出し関数名です。
これが例えばToIntFunction
などであれば"applyAsInt"
という文字列を渡すことになります。
// 下で指定しているインターフェースの呼び出しインターフェースの名前
"apply",
// site.target.invokeExact()した時に出てくるインターフェースのクラス
MethodType.methodType(Function.class),
今回は生成するインターフェースとしてFunction
を指定していますが、これは自分で作成したインターフェースなどでも大丈夫です。
MethodTypeの指定について
サンプルコードでは、LambdaMetafactory.metafactory
の第4・第6引数にMethodType
を渡しています。
これは関数の引数や戻り値の情報です。
第4引数にgeneric()
しているのは、これをやらないと例えばint
とInteger
のような場合に代入できないことが有るためです。
// 引数情報、generic()することでintとIntegerのような指定が齟齬を起こさなくなる
methodHandle.type().generic(),
// 呼ばれる関数のMethodHandle
methodHandle,
// 引数情報
methodHandle.type()
第4引数にのみgeneric()
して第6引数にしない理由は良く分かっていません。
見つけたコードが大体こうしていたのでそれをまねています。
幾つか試行錯誤した限りではこの形での指定が良さそうでした。
また、一時変数を使わずに2つの引数でそれぞれmethodHandle
からtype()
しているのは、実装を追った感じgeneric()
に副作用が有るように見えたためです。
複数引数を要求する場合
次に、SampleClass.sum
を題材に、複数引数を要求する場合について説明します。
SampleClass.sum
は5引数を要求しますが、用意されている関数型インターフェースは取れて2引数までです。
そのため、ここでは5引数入力可能なインターフェースを自分で用意する形で行います。
なお、FunctionalInterface
注釈は無くても動きます。
public interface Function5<P1, P2, P3, P4, P5, R> {
R invoke(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5);
}
生成方法や利用方法は先ほど説明した内容とほぼ同じです。
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();
}
}
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
)を利用します。
オプションは以下の通りです。
jmh {
failOnError = true
isIncludeTests = false
resultFormat = "CSV"
}
ベンチマークは以下のコードで行います。
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
で良くないか……?」と感じる点も多く有り、「使いこなせる気がしない」というのが感想です。
圧倒的に高速なことは確かなので、自作ライブラリに組み込めたらいいなとは思いますが……。
何にせよもう少し研究を続けてみます。
参考にさせて頂いた記事
- OptaPlanner - Java Reflection, but much faster
- Performance profiling ways of invoking a method dynamically - Development - Image.sc Forum
- Think Twice Before Using Reflection – CUBA Platform
- Hervian/lambda-factory: A fast alternative to Java Reflection API's method invocation