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