`MethodHandle` slower than Reflection when accessing Object

158 Views Asked by At

I would like to call a method via reflection in the most performant way possible.

The method returns an Object.

I've implemented this using both reflection and MethodHandles, I was expecting MethodHandle to be faster - but that's not what I'm seeing (~20-40% slower).

Take the following JMH benchmark:

import org.openjdk.jmh.annotations.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 200, time = 10, timeUnit = TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class AccessorBenchmark {
    private static final Object[] EMPTY_ARGS = new Object[0];

    private POJO source;
    private Method method;
    private MethodHandle methodHandle;
    private MethodHandle methodHandleModifiedReturnType;

    @Setup
    public void setup() throws ReflectiveOperationException {
        source = new POJO();

        method = source.getClass().getDeclaredMethod("getNumber");
        
        methodHandle = MethodHandles.lookup().unreflect(method);
        methodHandleModifiedReturnType = methodHandle.asType(methodHandle.type().changeReturnType(Number.class));
    }

    @Benchmark
    public Number reflection() throws Throwable {
        return (Number) method.invoke(source, EMPTY_ARGS);
    }

    @Benchmark
    public Number methodHandle() throws Throwable {
        return (Number) methodHandle.invoke(source);
    }

    @Benchmark
    public Number methodHandleInvokeExact() throws Throwable {
        return  (Number) methodHandleModifiedReturnType.invokeExact(source);
    }

    public class POJO {
        private final AtomicInteger counter = new AtomicInteger();

        public AtomicInteger getNumber() {
            return counter;
        }
    }
}

The following result is returned with Java 17:

Benchmark                                     Mode   Cnt   Score    Error   Units
AccessorBenchmark.methodHandle                avgt  1000   2.856 ±  0.004   ns/op
AccessorBenchmark.methodHandleInvokeExact     avgt  1000   2.359 ±  0.003   ns/op
AccessorBenchmark.reflection                  avgt  1000   2.017 ±  0.002   ns/op

Any ideas?

2

There are 2 best solutions below

4
scrhartley On

You need to make your method handles static final so that they can be constant folded (apparently).

See this article Java Reflection, but much faster, which explores reflection vs. method handle performance as you want.

0
DuncG On

See answer by scrhartley. Two additional benchmarks for direct call, and with lookup method handle directly as static final field (not via reflection) shows better results:

/** Test POJO directly */
@Benchmark
public Number direct() throws Throwable {
    return source.getNumber();
}
private static final Lookup     LOOKUP = MethodHandles.lookup();
/** Lookup details of an instance method handle on a Java class */
static MethodHandle findInstanceMH(Class<?> cls, String callback, MethodType methodType) {
    try {
        return LOOKUP.findVirtual(cls, callback, methodType);
    } catch (NoSuchMethodException | IllegalAccessException e) {
        throw new Error("Not found callback: "+callback);
    }
}
private static final MethodHandle MH  = findInstanceMH(POJO.class, "getNumber", MethodType.methodType(AtomicInteger.class));
private static final MethodHandle MHN = MH.asType(MH.type().changeReturnType(Number.class));

/** Test static final MethodHandle obtained by MH lookup */
@Benchmark
public Number methodHandleNormal() throws Throwable {
    return  (Number) MHN.invokeExact(source);
}

With the new benchmarks you should see method handle version much quicker than reflection and compares well with direct call:

Benchmark                                  Mode   Cnt   Score   Error  Units
AccessorBenchmark.direct                   avgt  1000   4.925 ± 0.047  ns/op
AccessorBenchmark.methodHandle             avgt  1000  10.327 ± 0.066  ns/op
AccessorBenchmark.methodHandleInvokeExact  avgt  1000   9.478 ± 0.066  ns/op
AccessorBenchmark.methodHandleNormal       avgt  1000   5.063 ± 0.054  ns/op
AccessorBenchmark.reflection               avgt  1000  17.477 ± 0.116  ns/op