background

When Javadeveloping, we will inevitably use a large number of reflection operations. For example, when Spring Bootreceiving HTTPa request, we will use reflection Controllerto call the corresponding method in the interface, or Jacksonthe framework will use reflection to parse jsonthe data and assign values ​​to the corresponding fields. , we can write a simple JMHtest to evaluate the performance gap between creating objects through reflective calls and directly calling object constructors:


@BenchmarkMode(value = Mode.AverageTime)  
@Warmup(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS)  
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)  
@State(Scope.Thread)  
@OutputTimeUnit(TimeUnit.NANOSECONDS)  
public abstract class JmhTest {  
    public static void runTest(Class<?> launchClass) throws RunnerException {  
        Options options = new OptionsBuilder().include(launchClass.getSimpleName()).build();  
        new Runner(options).run();  
    }  
}

package cn.zorcc.common.jmh;  
  
import org.openjdk.jmh.annotations.Benchmark;  
import org.openjdk.jmh.annotations.Param;  
import org.openjdk.jmh.infra.Blackhole;  
import org.openjdk.jmh.runner.RunnerException;  
  
import java.lang.invoke.MethodHandle;  
import java.lang.invoke.MethodHandles;  
import java.lang.invoke.MethodType;  
import java.lang.reflect.Constructor;  
import java.lang.reflect.Method;  
  
public class ReflectionTest extends JmhTest {  
    @Param({"10", "100", "1000", "10000"})  
    private int size;  
  
    static class Test {  
        private int integer;  
  
        public int getInteger() {  
            return integer;  
        }  
  
        public void setInteger(int integer) {  
            this.integer = integer;  
        }  
    }  
  
    @Benchmark  
    public void testDirectCall(Blackhole bh) {  
        for(int i = 0; i < size; i++) {  
            Test test = new Test();  
            bh.consume(test);  
            test.setInteger(i);  
            bh.consume(test.getInteger());  
        }  
    }  
  
    @Benchmark  
    public void testNormalReflection(Blackhole bh) {  
        try{  
            Constructor<Test> constructor = Test.class.getDeclaredConstructor();  
            Method setter = Test.class.getDeclaredMethod("setInteger", int.class);  
            Method getter = Test.class.getDeclaredMethod("getInteger");  
            for(int i = 0; i < size; i++) {  
                Test test = constructor.newInstance();  
                bh.consume(test);  
                setter.invoke(test, i);  
                int integer = (int) getter.invoke(test);  
                bh.consume(integer);  
            }  
        }catch (Throwable e) {  
            throw new UnknownError();  
        }  
    }  
  
  
    public static void main(String[] args) throws RunnerException {  
        runTest(ReflectionTest.class);  
    }  
}


In Testthe class, there is a simple inttype of variable. We test the performance comparison between directly calling the constructor, assigning and then getting the value, and using Constructorand making ordinary reflection calls. Be sure to use Methodthe constructed object. Blackhole.consume()This way, JVMunused variables will not be directly optimized and wrong test results will be obtained. The results of running the above code on the author’s machine are as follows:


Benchmark                                  (size)  Mode  Cnt       Score       Error  Units
ReflectionTest.testDirectCall                  10  avgt   50      10.584 ±     0.141  ns/op
ReflectionTest.testDirectCall                 100  avgt   50     108.301 ±     1.129  ns/op
ReflectionTest.testDirectCall                1000  avgt   50    1068.026 ±    12.312  ns/op
ReflectionTest.testDirectCall               10000  avgt   50   10660.596 ±   148.673  ns/op
ReflectionTest.testNormalReflection            10  avgt   50     145.483 ±     1.300  ns/op
ReflectionTest.testNormalReflection           100  avgt   50    1131.994 ±    19.586  ns/op
ReflectionTest.testNormalReflection          1000  avgt   50   13461.067 ±   130.624  ns/op
ReflectionTest.testNormalReflection         10000  avgt   50  148811.318 ±  5766.679  ns/op

It can be seen that there is a very big gap in the performance of using reflection compared to direct calling, especially in this extremely simple object creation scenario, but using reflection is a method we have to adopt in many cases, so we Is there any way to optimize the performance of reflection calls as much as possible?

Let us first try MethodHandlethe provided method calling model, MethodHandlewhich has JDK7been launched since the version. It is a new way to replace the old reflection calling. Compared with the original reflection calling, it provides more interaction methods and has the ability to support Javamethods. Calling Nativea consistent model with method calls, we can simply create a use case for testing:


@Benchmark  
public void testMethodHandleReflection(Blackhole bh) {  
    try{  
        MethodHandles.Lookup lookup = MethodHandles.lookup();  
        MethodType constructorType = MethodType.methodType(void.class);  
        MethodHandle constructorHandle = lookup.findConstructor(Test.class, constructorType);  
        MethodHandle iSetter = lookup.findSetter(Test.class, "integer", int.class);  
        MethodHandle iGetter = lookup.findGetter(Test.class, "integer", int.class);  
        for(int i = 0; i < size; i++) {  
            Test test = (Test) constructorHandle.invokeExact();  
            bh.consume(test);  
            iSetter.invokeExact(test, i);  
            int integer = (int) iGetter.invokeExact(test);  
            bh.consume(integer);  
        }  
    }catch (Throwable e) {  
        throw new UnknownError();  
    }  
}

The actual measurement results are even more unsatisfactory:


ReflectionTest.testMethodHandleReflection      10  avgt   50    1346.515 ±    17.347  ns/op
ReflectionTest.testMethodHandleReflection     100  avgt   50    2355.083 ±    37.358  ns/op
ReflectionTest.testMethodHandleReflection    1000  avgt   50  456694.572 ± 31415.118  ns/op
ReflectionTest.testMethodHandleReflection   10000  avgt   50  982008.110 ± 46807.572  ns/op

It can be seen that MethodHandlethe performance gap between using and using ordinary reflection is as big as the gap between ordinary reflection and direct call. In fact, in JDK18the future, according to #JEP 416: Reimplement Core Reflection with Method Handles, the correlation between using java.lang.reflectand Corresponding underlying refactoring was carried out and implemented using Define it as much as possible , so that it can be constant-folded, thereby achieving a huge performance improvement. Let us modify the above test code:java.lang.invokeAPIMethodHandlejava.lang.reflectjava.lang.invokeMethodHandleMethodHandlestatic finalJVM


private static final MethodHandle constructorHandle;  
private static final MethodHandle iSetter;  
private static final MethodHandle iGetter;  
static {  
    try{  
        MethodHandles.Lookup lookup = MethodHandles.lookup();  
        MethodType constructorType = MethodType.methodType(void.class);  
        constructorHandle = lookup.findConstructor(Test.class, constructorType);  
        iSetter = lookup.findSetter(Test.class, "integer", int.class);  
        iGetter = lookup.findGetter(Test.class, "integer", int.class);  
    }catch (Throwable e) {  
        throw new UnknownError();  
    }  
}

@Benchmark  
public void testMethodHandleReflection(Blackhole bh) {  
    try{  
        for(int i = 0; i < size; i++) {  
            Test test = (Test) constructorHandle.invokeExact();  
            bh.consume(test);  
            iSetter.invokeExact(test, i);  
            int integer = (int) iGetter.invokeExact(test);  
            bh.consume(integer);  
        }  
    }catch (Throwable e) {  
        throw new UnknownError();  
    }  
}

The following data was obtained:


ReflectionTest.testMethodHandleReflection      10  avgt   50       9.825 ±    0.084  ns/op
ReflectionTest.testMethodHandleReflection     100  avgt   50      99.174 ±    1.128  ns/op
ReflectionTest.testMethodHandleReflection    1000  avgt   50     997.094 ±   11.961  ns/op
ReflectionTest.testMethodHandleReflection   10000  avgt   50   10212.014 ±  215.662  ns/op

Suddenly, the performance of our reflection call and direct call are completely consistent, so does this mean that the function we want has been fully realized? In fact, this is not the case. If we have to static finalspecify the reflection fields that need to be used, it is equivalent to losing most of the flexibility, and the feasibility in actual operations is not high.

Similarly, we can give it a try, first build and cache the objects required for direct use of java.lang.reflectfunctions java.lang.invokelocally, and then test their corresponding performance:


private Constructor<Test> c;  
private Method setter;  
private Method getter;

@Setup   
public void setup() {
	try{  
	    this.c = Test.class.getDeclaredConstructor();  
		this.setter = Test.class.getDeclaredMethod("setInteger", int.class);  
		this.getter = Test.class.getDeclaredMethod("getInteger");
	}catch (Throwable e) {  
	    throw new UnknownError();  
	}
}

@Benchmark  
public void testNormalReflection(Blackhole bh) {  
    try{  
        for(int i = 0; i < size; i++) {  
            Test test = c.newInstance();  
            bh.consume(test);  
            setter.invoke(test, i);  
            int integer = (int) getter.invoke(test);  
            bh.consume(integer);  
        }  
    }catch (Throwable e) {  
        throw new UnknownError();  
    }  
}

MethodHandleUnlike when we define the variables that need to be initialized when testing static final, here we directly define them as privatevariables and initialize them in the functions JMHprovided in the framework @Setup, which more closely simulates the behavior we create at runtime. The test gets The result is as follows:


ReflectionTest.testNormalReflection      10  avgt   50     152.242 ±    5.625  ns/op
ReflectionTest.testNormalReflection     100  avgt   50    1495.302 ±   21.467  ns/op
ReflectionTest.testNormalReflection    1000  avgt   50   16917.774 ±  420.810  ns/op
ReflectionTest.testNormalReflection   10000  avgt   50  143252.377 ± 2150.908  ns/op

It can be seen that using ordinary reflection, whether it is to obtain a new object for creation each time Constructoror Methodto load it through advance caching, the performance is similar, which also makes the general reflection calling method applicable to all types of applications. It can perform relatively well in any scenario.

In view of our previous test results, if we want to further improve the performance of reflection, we can only consider using the class generation method. MethodHandleThe static variables created during compilation can be JVMautomatically inlined for us. Of course, the class generation method It must have very good performance, but the code for class generation using ByteBuddyor Asmframework is relatively cumbersome. Currently [# JEP 457: Class-File API (Preview)].( openjdk.org/jeps/457 ) The feature is in previewstage, which can help us more simplify JVMclass generation in , but we can’t use it yet.

solution

LambdaExpressions run through all corners of our daily development, and Lambdathe performance of the expression itself is not bad, otherwise JDKit would never be used so extensively internally. LambdaThe expression generation method is not complicated either. The core method behind it is Through LambdaMetafactory.metafactory()methods to generate corresponding method calls, we can implement the following code to complete the conversion of corresponding constructors, gettermethods and settermethods to Lambdafunctions:


private Supplier<Test> constructor;  
private BiConsumer<Object, Object> setConsumer;  
private Function<Test, Integer> getFunction;

@Setup  
public void setup() throws Throwable {
	MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(ReflectionTest.class, MethodHandles.lookup());  
	this.constructor = lambdaGenerateConstructor(lookup);  
	this.setConsumer = lambdaGenerateSetter(lookup);  
	this.getFunction = lambdaGenerateGetter(lookup);
}

@SuppressWarnings("unchecked")  
private Supplier<Test> lambdaGenerateConstructor(MethodHandles.Lookup lookup) throws Throwable {  
    MethodHandle cmh = lookup.findConstructor(Test.class, MethodType.methodType(void.class));  
    CallSite c1 = LambdaMetafactory.metafactory(lookup,  
            "get",  
            MethodType.methodType(Supplier.class),  
            MethodType.methodType(Object.class), cmh, MethodType.methodType(Test.class));  
    return (Supplier<Test>) c1.getTarget().invokeExact();  
}  
  
@SuppressWarnings("unchecked")  
private BiConsumer<Object, Object> lambdaGenerateSetter(MethodHandles.Lookup lookup) throws Throwable {  
    MethodHandle setHandle = lookup.findVirtual(Test.class, "setInteger", MethodType.methodType(void.class, int.class));  
    CallSite callSite = LambdaMetafactory.metafactory(lookup,  
            "accept",  
            MethodType.methodType(BiConsumer.class),  
            MethodType.methodType(void.class, Object.class, Object.class),  
            setHandle,  
            MethodType.methodType(void.class, Test.class, Integer.class));  
    return (BiConsumer<Object, Object>) callSite.getTarget().invokeExact();  
}  
  
@SuppressWarnings("unchecked")  
private Function<Test, Integer> lambdaGenerateGetter(MethodHandles.Lookup lookup) throws Throwable {  
    MethodHandle getHandle = lookup.findVirtual(Test.class, "getInteger", MethodType.methodType(int.class));  
    CallSite getSite = LambdaMetafactory.metafactory(  
            lookup,  
            "apply",  
            MethodType.methodType(Function.class),  
            MethodType.methodType(Object.class, Object.class),  
            getHandle,  
            MethodType.methodType(Integer.class, Test.class)  
    );  
    return (Function<Test, Integer>) getSite.getTarget().invokeExact();  
}

@Benchmark  
public void testLambda(Blackhole bh) {  
    for(int i = 0; i < size; i++) {  
        Test test = constructor.get();  
        bh.consume(test);  
        setConsumer.accept(test, i);  
        int integer = getFunction.apply(test);  
        bh.consume(integer);  
    }  
}  
  
@Benchmark  
public void testLambdaGeneration(Blackhole bh) throws Throwable {  
    MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(ReflectionTest.class, MethodHandles.lookup());  
    bh.consume(lambdaGenerateConstructor(lookup));  
    bh.consume(lambdaGenerateSetter(lookup));  
    bh.consume(lambdaGenerateGetter(lookup));  
}

The test is divided into two steps. One is to test Lambdathe generation performance of the expression, and the other is to test Lambdathe running performance of the expression. These two indicators are very important to us. The results are as follows:


ReflectionTest.testLambdaGeneration   10000  avgt   50   92486.909 ±  62638.147  ns/op

Benchmark                  (size)  Mode  Cnt      Score     Error  Units
ReflectionTest.testLambda      10  avgt   50     10.720 ±   0.087  ns/op
ReflectionTest.testLambda     100  avgt   50    105.001 ±   1.312  ns/op
ReflectionTest.testLambda    1000  avgt   50   1020.406 ±   9.990  ns/op
ReflectionTest.testLambda   10000  avgt   50  10198.842 ± 143.259  ns/op

It can be seen that by simulating Lambdaexpression generation, the performance of calling constructors and getmethods setis almost exactly the same as direct calling. This achieves the effect we want, but the Lambdagenerated performance is very unoptimistic. The performance of directly using arrow functions to generate is very different. Fortunately, if Lambdathe expression does not capture any external variables, such as the getand setmethod we called in the example, then the generated method can be cached and reused. If you use The base itself is relatively large, and in the cost trade-off of multiple calls, the initialization overhead can be ignored.

summary

This article introduces a Javanew reflection calling method in , that is, using Lambdaa method similar to expression generation for reflection. Some simple methods, such as getand setmethods, can be directly converted into corresponding Lambdaexpressions for calling. Although it can It can achieve the same performance as direct calling, but the generation overhead of this method is relatively large, and it needs to be cached in frequent calling scenarios to achieve better results.

Leave a Reply

Your email address will not be published. Required fields are marked *