javaperformancereflectionfieldmethodhandle

How can I improve performance of Field.set (perhap using MethodHandles)?


I'm writing some code that calls Field.set and Field.get many many thousands of times. Obviously this is very slow because of the reflection.

I want to see if I can improve performance using MethodHandle in Java 7. So far here's what I have:

Instead of field.set(pojo, value), I'm doing:

private static final Map<Field, MethodHandle> setHandles = new HashMap<>();

MethodHandle mh = setHandles.get(field);
if (mh == null) {
    mh = lookup.unreflectSetter(field);
    setHandles.put(field, mh);
}
mh.invoke(pojo, value);

However, this doesn't seem to perform better than the Field.set call using reflection. Am I doing something wrong here?

I read that using invokeExact could be faster but when I tried using that I got a java.lang.invoke.WrongMethodTypeException.

Has anyone successfully been able to optimize repeated calls to Field.set or Field.get?


Solution

  • 2015-06-01: Updated to reflect @JoeC's comment about another case when handles are static. Also updated to latest JMH and re-ran on modern hardware. The conclusion stays almost the same.

    Please do proper benchmarking, it is arguably not that hard with JMH. Once you do that, the answer becomes obvious. It can also showcase the proper use of invokeExact (requires target/source 1.7 to compile and run):

    @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
    @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
    @Fork(3)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Thread)
    public class MHOpto {
    
        private int value = 42;
    
        private static final Field static_reflective;
        private static final MethodHandle static_unreflect;
        private static final MethodHandle static_mh;
    
        private static Field reflective;
        private static MethodHandle unreflect;
        private static MethodHandle mh;
    
        // We would normally use @Setup, but we need to initialize "static final" fields here...
        static {
            try {
                reflective = MHOpto.class.getDeclaredField("value");
                unreflect = MethodHandles.lookup().unreflectGetter(reflective);
                mh = MethodHandles.lookup().findGetter(MHOpto.class, "value", int.class);
                static_reflective = reflective;
                static_unreflect = unreflect;
                static_mh = mh;
            } catch (IllegalAccessException | NoSuchFieldException e) {
                throw new IllegalStateException(e);
            }
        }
    
        @Benchmark
        public int plain() {
            return value;
        }
    
        @Benchmark
        public int dynamic_reflect() throws InvocationTargetException, IllegalAccessException {
            return (int) reflective.get(this);
        }
    
        @Benchmark
        public int dynamic_unreflect_invoke() throws Throwable {
            return (int) unreflect.invoke(this);
        }
    
        @Benchmark
        public int dynamic_unreflect_invokeExact() throws Throwable {
            return (int) unreflect.invokeExact(this);
        }
    
        @Benchmark
        public int dynamic_mh_invoke() throws Throwable {
            return (int) mh.invoke(this);
        }
    
        @Benchmark
        public int dynamic_mh_invokeExact() throws Throwable {
            return (int) mh.invokeExact(this);
        }
    
        @Benchmark
        public int static_reflect() throws InvocationTargetException, IllegalAccessException {
            return (int) static_reflective.get(this);
        }
    
        @Benchmark
        public int static_unreflect_invoke() throws Throwable {
            return (int) static_unreflect.invoke(this);
        }
    
        @Benchmark
        public int static_unreflect_invokeExact() throws Throwable {
            return (int) static_unreflect.invokeExact(this);
        }
    
        @Benchmark
        public int static_mh_invoke() throws Throwable {
            return (int) static_mh.invoke(this);
        }
    
        @Benchmark
        public int static_mh_invokeExact() throws Throwable {
            return (int) static_mh.invokeExact(this);
        }
    
    }
    

    On 1x4x2 i7-4790K, JDK 8u40, Linux x86_64 it yields:

    Benchmark                             Mode  Cnt  Score   Error  Units
    MHOpto.dynamic_mh_invoke              avgt   25  4.393 ± 0.003  ns/op
    MHOpto.dynamic_mh_invokeExact         avgt   25  4.394 ± 0.007  ns/op
    MHOpto.dynamic_reflect                avgt   25  5.230 ± 0.020  ns/op
    MHOpto.dynamic_unreflect_invoke       avgt   25  4.404 ± 0.023  ns/op
    MHOpto.dynamic_unreflect_invokeExact  avgt   25  4.397 ± 0.014  ns/op
    MHOpto.plain                          avgt   25  1.858 ± 0.002  ns/op
    MHOpto.static_mh_invoke               avgt   25  1.862 ± 0.015  ns/op
    MHOpto.static_mh_invokeExact          avgt   25  1.859 ± 0.002  ns/op
    MHOpto.static_reflect                 avgt   25  4.274 ± 0.011  ns/op
    MHOpto.static_unreflect_invoke        avgt   25  1.859 ± 0.002  ns/op
    MHOpto.static_unreflect_invokeExact   avgt   25  1.858 ± 0.002  ns/op
    

    ...which suggests MH are really much faster than Reflection in this particular case (this is because the access checks against the private field is done at lookup time, and not at the invocation time). dynamic_* cases simulate the case when the MethodHandles and/or Fields are not statically known, e.g. pulled from Map<String, MethodHandle> or something like it. Conversely, static_* cases are those where the invokers are statically known.

    Notice the reflective performance is on par with MethodHandles in dynamic_* cases, this is because reflection is heavily optimized further in JDK 8 (because really, you don't need the access check to read your own fields), so the answer may be "just" switching to JDK 8 ;)

    static_* cases are even faster, because the MethoHandles.invoke calls are aggressively inlined. This eliminates part of the type checking in MH cases. But, in reflection cases, there are still quick checks present, and therefore, it lags behind.