Android MethodHandle reflection performance optimization

Posted by willdk on Sat, 05 Mar 2022 02:39:08 +0100

1, About instruction call:

Whether it is a stack based JVM or a register based DVM, except for the movement and space allocation of variables in the operand stack and the difference of program counters, the basic instructions of basic method calls comply with the JSR specification.

Before discussing MethodHandle, let's talk about Java Dynamic defects. Java language is a static language, and its dynamic consistency has many limitations:

  • Generic defects: generic types are erased during front-end compilation (javac), generic types cannot be passed, generic types do not support value types, and dynamic generic types are not supported
  • Dynamic method substitution is not supported
  • Dynamic proxies for normal classes are not supported
  • The tool API for generating bytecode is relatively cumbersome
  • There are too many reflection call checks, and a large number of bytecodes need to be generated

MethodHandle mainly solves the following problems:

[1] For reflection optimization, MethodHandle belongs to lightweight direct call bytecode, while reflection belongs to heavyweight and cannot replace reflection at all

[2] Dynamic method dispatch can make the subclass call the method of the parent class, even if the method is copied by the subclass.

[3] Call other languages running on the JVM

 

As we know, the JSR standard provides four instructions for calling methods

  • invokestatic: call static methods (JIT will optimize this instruction inlining, and there is no need to guard inlining)
  • invokevirtual: calling virtual methods, which generally refers to non private methods (JIT will guard such calls, inline and cache inline. Of course, cache inline also needs to be guarded, otherwise the trap may not escape)
  • invokespecial: call constructors and non virtual methods (JIT will optimize this kind of instruction inlining, and there is no need to guard inlining)
  • invokeinterface: call interface methods (JIT optimization does not depend on this call, but its implementation invokevirtual)

MethodHandle is essentially used to call other languages in the JVM. In fact, it also has the ability of method dispatch (Note: only direct parent classes can be dispatched, and it can be dispatched arbitrarily before the JSR version is modified)

public class MethodHandleDynamicInvoke {

    public static void main(String[] args) {
        Son son = new Son();
        son.love(Father.class);
        son.love(Son.class);
    }
}

class Father   {
    String thinking() {
        return "Father";
    }
}

class Son extends Father {
    @Override
    String thinking() {
        return "Son";
    }

    public void love(Class target) {
        MethodType methodType = MethodType.methodType(String.class);
        try {
            MethodHandle thinkingMethodHanle = MethodHandles.lookup().findSpecial(target, "thinking", methodType, Son.class).bindTo(Son.this);
            System.out.println("I love " + ((String) thinkingMethodHanle.invokeExact()));
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

The output results are as follows

I love Father
I love Son

2, How does MethodHandle optimize reflection?

In the previous section, we learned about the problems solved by JVM related instructions and MethodHandle. Next, we introduce the following important API s

  • MethodHandles.lookup() ; Method query
  • MethodType.methodType(....); It is used for return value, parameter matching and dynamic parameters. The first parameter is the return value type and the second parameter is the method parameter
  • MethodHandle.bindTo(Object obj) ; Bind the execution object. After binding, invoke is alive. The first parameter of invokeExact cannot be passed to the current object
  • MethodHanle.invoke(...) ; This method belongs to the fuzzy parameter type, and the parameters can be passed into the parent type. Of course, the cost is that the execution efficiency of this method is quite low
  • MethodHandle.invokeExact(...) ; The parameter type method must be completely consistent with the called method. If it is a String, it must be converted to String and cannot be replaced by Object. If necessary, type conversion is carried out. The benefit is that the execution efficiency of this method is high
  • Lookup findConstructor ,findSpecial ,findVirtual ,findStatic ,findXXXGetter,findXXXSetter ... It is used for method query, in which findspecial queries non virtual methods and findstatic queries static methods
  • Lookup unreflectXXX is used in conjunction with reflection, because MethodHandle cannot completely replace reflection. For example, calling private methods in different packages and class es needs to be reflected and then converted to MethodHandle

How does MethodHandle achieve performance optimization?

  • MethodHandle static
  • The performance of bindTo in advance will be higher
  • invokeExact

Let's give an example to define a private method to improve reflection performance

public class ClassMethodHandle {
    private   int getDoubleVal(int val) {
        return val * 2;
    }
}

The test code is as follows


public class MethodHandleTest   {
    static  Method method_getDoubleVal;
    static MethodHandle methodHandle_getDoubleVal;
    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            method_getDoubleVal =  ClassMethodHandle.class.getDeclaredMethod("getDoubleVal",int.class);
            method_getDoubleVal.setAccessible(true);
            methodHandle_getDoubleVal = lookup.unreflect(method_getDoubleVal); 
        //Private methods cannot be accessed through findSpecial. Method Reflection is required
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }
    public static void main(String[] args) throws Throwable {

        long startTime  = System.nanoTime();
        testMethodReflection();
        long dx1 = System.nanoTime() - startTime;
        System.out.println("[1] " + (dx1));

        startTime  = System.nanoTime();
        testMethodHandle();
        long dx2 = System.nanoTime() - startTime;
        System.out.println("[2] " + (dx2));

        System.out.println(">>"+(dx1*1f)/dx2+"<<");

    }

    private static void testMethodReflection() {
        try {

             List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
            for (int i=0;i<5;i++) {
                MethodHandleTest.transformRelection(dataList,method_getDoubleVal,new ClassMethodHandle());
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    private static void testMethodHandle() throws Throwable {

        List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
        for (int i=0;i<5;i++) {
            MethodHandleTest.transform(dataList, methodHandle_getDoubleVal, new ClassMethodHandle());// Method as a parameter
        }

    }

    public static List<Integer> transformRelection(List<Integer> dataList, Method method,Object obj) throws Throwable {
        for (int i = 0; i < dataList.size(); i++) {
            dataList.set(i, (int) method.invoke(obj,dataList.get(i)));//relect invoke
        }
        return dataList;
    }

    public static List<Integer> transform(List<Integer> dataList, MethodHandle handle,Object obj) throws Throwable {
        for (int i = 0; i < dataList.size(); i++) {
            dataList.set(i, (int) handle.invokeExact((ClassMethodHandle)obj,(int)dataList.get(i)));//Pay attention to parameter type conversion
        }
        return dataList;  
    }  
}

The performance test results are as follows, taking the maximum of 5 times

[1] 2952626
[2] 1031642
 >>2.8620646<<

What if you bind in advance?


public class MethodHandleTest {
    static Method method_getDoubleVal;
    static MethodHandle methodHandle_getDoubleVal;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            method_getDoubleVal = ClassMethodHandle.class.getDeclaredMethod("getDoubleVal", int.class);
            method_getDoubleVal.setAccessible(true);
            methodHandle_getDoubleVal = lookup.unreflect(method_getDoubleVal).bindTo(new ClassMethodHandle());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws Throwable {

        long startTime = System.nanoTime();
        testMethodReflection();
        long dx1 = System.nanoTime() - startTime;
        System.out.println("[1] " + (dx1));

        startTime = System.nanoTime();
        testMethodHandle();
        long dx2 = System.nanoTime() - startTime;
        System.out.println("[2] " + (dx2));

        System.out.println(" "+((dx1*1f)/dx2));

    }

    private static void testMethodReflection() {
        try {
            ClassMethodHandle classMethodHandle = new ClassMethodHandle();
            List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
            for (int i = 0; i < 5; i++) {
                MethodHandleTest.transformRelection(dataList, method_getDoubleVal, classMethodHandle);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    private static void testMethodHandle() throws Throwable {
        List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
        for (int i = 0; i < 5; i++) {
            MethodHandleTest.transform(dataList, methodHandle_getDoubleVal);// Method as a parameter
        }

    }

    public static List<Integer> transformRelection(List<Integer> dataList, Method method, Object obj) throws Throwable {
        for (int i = 0; i < dataList.size(); i++) {
            dataList.set(i, (int) method.invoke(obj, dataList.get(i)));//relect invoke
        }
        return dataList;
    }

    public static List<Integer> transform(List<Integer> dataList, MethodHandle handle) throws Throwable {
        for (int i = 0; i < dataList.size(); i++) {
            dataList.set(i, (int) handle.invokeExact((int) dataList.get(i)));//Pay attention to parameter type conversion
        }
        return dataList;
    }
}

Performance test, take the maximum of 5 times

[1] 5893674
[2] 1279878
 4.6048717