1

I have constructed a simple test to measure the performance of class reflection with the Java LambdaMetafactory. According to various posts reflection using LambdaMetafactory is as fast as directly calling getters. This seems true initially, but after a while performance degrades.

One of the test shows (this seems general trend):

Initially:

GET - REFLECTION: Runtime=2.841seconds
GET - DIRECT:     Runtime=0.358seconds
GET - LAMBDA:     Runtime=0.362seconds
SET - REFLECTION: Runtime=3.86seconds
SET - DIRECT:     Runtime=0.507seconds
SET - LAMBDA:     Runtime=0.455seconds

Finally:

GET - REFLECTION: Runtime=2.904seconds
GET - DIRECT:     Runtime=0.501seconds
GET - LAMBDA:     Runtime=5.299seconds
SET - REFLECTION: Runtime=4.62seconds
SET - DIRECT:     Runtime=1.723seconds
SET - LAMBDA:     Runtime=5.149seconds

Code follows below.

Questions:

  • Why?

  • How can this be made more performant with the LambdaMetafactory?


package lambda;

public class Main {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, Throwable {
        int runs = 5;

        for (int i = 0; i < runs; i++) {
            System.out.println("***** RUN " + i);

            Lambda lam = new Lambda();
            lam.initGetter(Person.class, "getName");
            lam.initSetter(Person.class, "setSalary", double.class);

            Test test = new Test();
            test.doTest(lam);
        }

    }
}

package lambda;

import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.function.BiConsumer;
import java.util.function.Function;

public final class Lambda {

    //https://www.optaplanner.org/blog/2018/01/09/JavaReflectionButMuchFaster.html
    //https://bytex.solutions/2017/07/java-lambdas/
    //https://stackoverflow.com/questions/27602758/java-access-bean-methods-with-lambdametafactory
    private Function getterFunction;
    private BiConsumer setterFunction;

    private Function createGetter(final MethodHandles.Lookup lookup,
            final MethodHandle getter) throws Exception {
        final CallSite site = LambdaMetafactory.metafactory(lookup, "apply",
                MethodType.methodType(Function.class),
                MethodType.methodType(Object.class, Object.class), //signature of method Function.apply after type erasure
                getter,
                getter.type()); //actual signature of getter
        try {
            return (Function) site.getTarget().invokeExact();
        } catch (final Exception e) {
            throw e;
        } catch (final Throwable e) {
            throw new Exception(e);
        }
    }

    private BiConsumer createSetter(final MethodHandles.Lookup lookup,
            final MethodHandle setter) throws Exception {
        final CallSite site = LambdaMetafactory.metafactory(lookup,
                "accept",
                MethodType.methodType(BiConsumer.class),
                MethodType.methodType(void.class, Object.class, Object.class), //signature of method BiConsumer.accept after type erasure
                setter,
                setter.type()); //actual signature of setter
        try {
            return (BiConsumer) site.getTarget().invokeExact();
        } catch (final Exception e) {
            throw e;
        } catch (final Throwable e) {
            throw new Exception(e);
        }
    }

    public void initGetter(Class theSubject, String methodName) throws ReflectiveOperationException, Exception {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        Method m = theSubject.getMethod(methodName);
        MethodHandle mh = lookup.unreflect(m);

        getterFunction = createGetter(lookup, mh);
    }

    public void initSetter(Class theSubject, String methodName, Class parameterType) throws ReflectiveOperationException, Exception {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        Method m = theSubject.getMethod(methodName, parameterType);
        MethodHandle mh = lookup.unreflect(m);

        setterFunction = createSetter(lookup, mh);
    }

    public Object executeGetter(Object theObject) {
        return getterFunction.apply(theObject);
    }

    public void executeSetter(Object theObject, Object value) {
        setterFunction.accept(theObject, value);
    }

}

package lambda;

import java.lang.reflect.Field;

public class Test {

    public void doTest(Lambda lam) throws NoSuchFieldException, IllegalArgumentException, Exception {

        if (lam == null) {
            lam = new Lambda();
            lam.initGetter(Person.class, "getName");
            lam.initSetter(Person.class, "setSalary", double.class);
        }

        Person p = new Person();
        p.setName(111);

        long loops = 1000000000; // 10e9
        System.out.println("Loops: " + loops);

        ///
        System.out.println("GET - REFLECTION:");

        Field field = Person.class.getField("name");

        long start = System.currentTimeMillis();
        for (int i = 0; i < loops; i++) {
            int name = (int) field.get(p);
        }
        long end = System.currentTimeMillis();
        System.out.println("Runtime=" + ((end - start) / 1000.0) + "seconds");

        ///
        System.out.println("GET - DIRECT:");
        start = System.currentTimeMillis();
        for (int i = 0; i < loops; i++) {
            int name = (int) p.getName();
        }
        end = System.currentTimeMillis();
        System.out.println("Runtime=" + ((end - start) / 1000.0) + "seconds");

        ////
        System.out.println("GET - LAMBDA:");
        start = System.currentTimeMillis();
        for (int i = 0; i < loops; i++) {
            int name = (int) lam.executeGetter(p);
        }
        end = System.currentTimeMillis();
        System.out.println("Runtime=" + ((end - start) / 1000.0) + "seconds");

        ///
        System.out.println("SET - REFLECTION:");
        int name = 12;

        start = System.currentTimeMillis();
        for (int i = 0; i < loops; i++) {
            field.set(p, name);
        }
        end = System.currentTimeMillis();
        System.out.println("Runtime=" + ((end - start) / 1000.0) + "seconds");

        ///
        System.out.println("SET - DIRECT:");
        name = 33;
        start = System.currentTimeMillis();
        for (int i = 0; i < loops; i++) {
            p.setName(name);
        }
        end = System.currentTimeMillis();
        System.out.println("Runtime=" + ((end - start) / 1000.0) + "seconds");

        ////
        System.out.println("SET - LAMBDA:");

        Double name2 = 2.3;

        start = System.currentTimeMillis();
        for (int i = 0; i < loops; i++) {
            lam.executeSetter(p, name2);
        }
        end = System.currentTimeMillis();
        System.out.println("Runtime=" + ((end - start) / 1000.0) + "seconds");
    }

}

package lambda;

public class Person {

    public int name;
    private double salary;

    public int getName() {
        return name;
    }

    public void setName(int name) {
        this.name = name;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

}

As mentioned later in comments I have updated the code to run as a self-contained test that I can invoke from any point in my application. The test accepts a class, method name (string), and a class instance in order to run the performance test. As expected performance is fine when I just use one class (Person1). As soon as I invoke the test for another class (Person2) performance drops for both classes -- sometimes performance is even worse than normal method reflect calls. Below some data. Initially performance is okay for Person1. As soon as Person2 is used there is a drop.

(Class=Person1, name=Name)
Loops: 50000000
GET - DIRECT: Runtime=0.016seconds
GET - LAMBDA: Runtime=0.016seconds

(Class=Person2, name=Name)
Loops: 50000000
GET - DIRECT: Runtime=0.019seconds
createLambda: new getter for clazz=lambda.Person2 name=Name
createLambda: new setter for clazz=lambda.Person2 name=Name
GET - LAMBDA: Runtime=0.069seconds

(Class=Person1, name=Name)
Loops: 50000000
GET - DIRECT: Runtime=0.02seconds
GET - LAMBDA: Runtime=0.045seconds

(Class=Person2, name=Name)
Loops: 50000000
GET - DIRECT: Runtime=0.017seconds
GET - LAMBDA: Runtime=0.054seconds

(Class=Person1, name=Name)
Loops: 50000000
GET - DIRECT: Runtime=0.018seconds
GET - LAMBDA: Runtime=0.045seconds
Talenel
  • 422
  • 2
  • 6
  • 25
bergtwvd
  • 165
  • 1
  • 9
  • 1
    You should use JMH for these kinds of microbenchmarks. – David Conrad Apr 20 '18 at 16:04
  • Most likely the JIT is getting confused by your benchmark and not optimising it properly. I suggest placing each test in it's own method to avoid a poor optimisation when the method is compiled (or use JMH which does this) – Peter Lawrey Apr 20 '18 at 16:24
  • I reduced the test to just the Lambda get. Next, if I run test the test with the same Lambda object (created in Main) across all runs then performance is stable. Otherwise it decreases to some value. Guess this prevents the poor optimization; but I am still guessing why. – bergtwvd Apr 21 '18 at 06:39
  • I am still puzzling about the performance of reflection using the LambdaMetafactory. I have slightly updated the example above to run as a self-contained test that I can invoke at any point in my application. Performance is as expected (direct vs lambda) whenI use it on just a single class, but drops as soon as I do the lambda getter for another Java class. I wonder if code size or other factors are in play that influence (negatively) performance. – bergtwvd May 12 '18 at 21:34
  • Please see: https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java – lexicore May 12 '18 at 22:28
  • Ok -- I have added a warmup period for every test. For the getter test on the Person1 and Person2 classes results are as expected (Direct/Lambda are comparable in performance). However, I still see performance collapse with a factor of 10 once I do the getter test on the third class named Person3. If there is space somewhere I can drop the code for anyone to try out too. – bergtwvd May 13 '18 at 01:00
  • I have restated the question in https://stackoverflow.com/questions/50321114/java-reflection-performance-alternatives, since the test program has changed and I would not want to update the text here. – bergtwvd May 13 '18 at 22:44

0 Answers0