[Java] Lambda Metafactory speeds up function calls by reflection to the direct call level

This article is the 16th day article of MicroAd \ (MicroAd ) Advent Calendar 2020.

Preface

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.

How to use Lambda Metafactory

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;
    }
}

Example to apply to static method/getter of 1 argument

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.

About getting MethodHandles.Lookup

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 ().

About generating CallSite using Lambda Metafactory

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.

About the interface generated with "apply"

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.

About specifying Method Type

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.

When requesting multiple arguments

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());
    }
}

benchmark

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.

Results and discussion

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

At the end

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.

Articles that I used as a reference

Recommended Posts

[Java] Lambda Metafactory speeds up function calls by reflection to the direct call level
[Java] How to use the hasNext function
[Processing × Java] How to use the function
[Java] Dynamic method call by reflection of enum (enum)
How to call functions in bulk with Java reflection
I want to call the main method using reflection