This article is the 16th day article of MicroAd \ (MicroAd ) Advent Calendar 2020.
Function calls with Java
reflections are slower than direct calls.
For example, in the verification in the following article, when calling a 5-argument constructor, the Constructor
call by reflection has a score of about 1/5 of the direct call.
-[Kotlin] Call KFunction at high speed (Part 1) -Qiita
In this article, we will use LambdaMetafactory
to generate CallSite
from Constructor
/ Method
and speed up the call by calling it.
The environment is Java8
.
It's like writing while studying, so there may be incorrect descriptions. I would appreciate it if you could point out.
The entire project is on GitHub
.
Benchmarks described below can be started with ./gradlew jmh
.
For the sake of explanation, we will use the following class that defines the constructor and factory method.
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;
}
}
Below is an example of code that uses LambdaMetafactory
to return a Function
that can be called quickly for a single argument, Method
.
You can use it in the constructor as well by changing the argument to the constructor and changing lookup.unreflect
to 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 is disposable or public Lookup due to security concerns()Looks good to use
MethodHandles.Lookup lookup = MethodHandles.lookup();
//Lookup for Constructor.Use unreflect Constructor
MethodHandle methodHandle = lookup.unreflect(method);
CallSite callSite = LambdaMetafactory.metafactory(
lookup,
//The name of the calling interface of the interface specified below
"apply",
// site.target.invokeExact()The interface class that appears when you do
MethodType.methodType(Function.class),
//Argument information, generic()By doing so, specifications such as int and Integer will not be inconsistent.
methodHandle.type().generic(),
//MethodHandle of the called function
methodHandle,
//Argument information
methodHandle.type()
);
//uncheck cast is required
@SuppressWarnings("unchecked")
Function<T, R> function = (Function<T, R>) callSite.getTarget().invokeExact();
return function;
}
}
This can be applied, for example, to a static
function with one argument or a getter
that takes an instance as an argument for execution.
Example of use
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 {
//Example applied to static function
@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());
}
//Example applied to 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));
}
}
Below, I will explain using these codes.
MethodHandles.Lookup
is used to manage accessibility such as private
and package private
in the scope described here.
For example, if the function in SampleClass
is private
, execute the function in this way unless you use MethodHandles.Lookup
obtained byMethodHandles.lookup ()
in SampleClass
. I can not do it.
It didn't work even if I Method.setAccessible
before and after.
There seemed to be a workaround for Java 9
and later, but it seemed to be a API
that wasn't available in Java 8
.
In the comments in the sample code, I added that "lookup is disposable or publicLookup () seems to be good because of security concerns
", but MethodHandles.Lookup
retains the information in the context. So it's probably not a good idea to keep it as a field.
Even if you keep it, I think it is better to save only the public
contents withMethodHandles.publicLookup ()
.
CallSite
is like Lambda
, the entity that is called in this speedup.
There are metafactory
and altMetafactory
as functions to generate CallSite
from LambdaMetafactory
, but this time metafactory
because the generation using metafactory
is faster [^ kousoku]. I will explain in the form of using.
[^ kousoku]: From Performance profiling ways of invoking a method dynamically -Development -Image \ .sc Forum.
In the sample code, the character string " apply "
is specified as an argument as shown below.
This is the name of the calling function of Function
specified below.
If this is, for example, ToIntFunction
, you will pass the string"applyAsInt"
.
Excerpt from sample code
//The name of the calling interface of the interface specified below
"apply",
// site.target.invokeExact()The interface class that appears when you do
MethodType.methodType(Function.class),
This time, Function
is specified as the interface to be generated, but this can be an interface created by yourself.
In the sample code, MethodType
is passed to the 4th and 6th arguments of LambdaMetafactory.metafactory
.
This is information about function arguments and return values.
The reason for generic ()
in the 4th argument is that if you do not do this, you may not be able to assign in cases such as int
and Integer
.
Excerpt from sample code
//Argument information, generic()By doing so, specifications such as int and Integer will not be inconsistent.
methodHandle.type().generic(),
//MethodHandle of the called function
methodHandle,
//Argument information
methodHandle.type()
I'm not sure why it's only the 4th argument generic ()
and not the 6th argument.
Code I found did this, so I'm imitating it.
As far as some trial and error was done, the designation in this form seemed to be good.
Also, the reason why the two arguments are type ()
from methodHandle
without using temporary variables is thatgeneric ()
seems to have a side effect.
Next, we will explain the case of requesting multiple arguments using SampleClass.sum
as a subject.
SampleClass.sum
requires 5 arguments, but the provided functional interface can take up to 2 arguments.
Therefore, here, we will prepare an interface that allows input of 5 arguments.
It works without the FunctionalInterface
annotation.
public interface Function5<P1, P2, P3, P4, P5, R> {
R invoke(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5);
}
The generation method and usage method are almost the same as those explained earlier.
import java.lang.invoke.*;
import java.lang.reflect.Method;
import java.util.function.Function;
public class LambdaMetaFactoryWrapper {
/*toOptimizedFunction function, omitted because it has already been explained*/
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", //Since Function5 is called by invoke, the argument is also"invoke"Has changed to
MethodType.methodType(Function5.class),
methodHandle.type().generic(),
methodHandle,
methodHandle.type()
);
//noinspection unchecked
return (Function5<P1, P2, P3, P4, P5, R>) callSite.getTarget().invokeExact();
}
}
Example of use
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 {
/*Omitted because it has already been explained*/
//Example applied to a static function with 5 arguments
@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());
}
}
Finally, for the 5-argument static
method (SampleClass.sum
), compare the three types of direct call, Method
call, and fast call by LambdaMetafactory
with the benchmark by JMH
.
Use JMH Gradle Plugin
(me.champeau.gradle.jmh
) for benchmarking.
The options are:
kotlin:build.gradle.kts (excerpt)
jmh {
failOnError = true
isIncludeTests = false
resultFormat = "CSV"
}
Benchmark is done with the following code.
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);
}
}
As mentioned at the beginning, these contents have been uploaded to GitHub
, and the benchmark can be started with ./gradlew jmh
.
The following is the measurement result of the Ryzen 7 3700X
at hand in the Windows
environment, the higher the score, the better.
As a result, you can see that the direct call is the best, and the method introduced is the second highest score, which is overwhelmingly faster than calling 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
This time, I introduced how to generate CallSite
from Constructor
/Method
using Lambda Metafactory
and speed up the call by calling it.
Because I didn't have enough knowledge, I was just wondering why it didn't work, but I'm glad I managed to make it seem to work.
However, there are many parts that I cannot grasp due to my lack of knowledge, and there are many points that I feel "Is this Constructor
/Method
good ...?", And my impression is that "I don't feel like I can master it."
It is certain that it is overwhelmingly fast, so I think it would be nice if it could be incorporated into the self-made library.
Anyway, I will continue my research a little more.
Recommended Posts