Published 06 Sep, 2022

Java - Calling a getter in Java though reflection: What's the fastest way to repeatedly call it (performance and scalability wise)?

Category Java
Modified : Oct 04, 2022
47

Given a class Foo and a property bar, neither of which I know at compile time, I need to repeatedly call the getter Foo.getBar() many, many times.

Suppose I have:

Method barGetterMethod = ...; // Don't worry how I got this

And I need to do something like this:

for (Object foo : fooList) { // 1000000000 elements in fooList
    Object bar = barGetterMethod.invoke(foo);
    ...
}

The implementation above is still very slow compared to calling it without reflection. Is there a faster way?

What's the fastest way of calling a getter with reflection in Java?

Answers

There are 3 suggested solutions here and each one has been listed below with a detailed description. The following topics have been covered briefly such as Performance, Java, Reflection. These have been categorized in sections for a clear and precise explanation.

73

You might use a MethodHandle. Its Javadoc writes:

Using factory methods in the Lookup API, any class member represented by a Core Reflection API object can be converted to a behaviorally equivalent method handle. For example, a reflective Method can be converted to a method handle using Lookup.unreflect. The resulting method handles generally provide more direct and efficient access to the underlying class members.

While this will reduce the overhead, method handles still prevent certain optimizations (such a method inlining) the JVM could employ if the call were made with the usual (non-reflective) byte code instructions. Whether such optimizations would be beneficial depends on how you use the method (if that code path always invokes the same method, inlining can help, if it is a different method each time, probably not).

The following microbenchmark might give you a rough idea about the relative performance of reflection, method handles, and direct invocation:

package tools.bench;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.math.BigDecimal;

public abstract class Bench {

    final String name;

    public Bench(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1; 
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }   

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    static class C {
        public Integer foo() {
            return 1;
        }
    }

    static final MethodHandle sfmh;

    static {
        try {
            Method m = C.class.getMethod("foo");
            sfmh = MethodHandles.lookup().unreflect(m);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws Exception {
        final C invocationTarget = new C();
        final Method m = C.class.getMethod("foo");
        final Method am = C.class.getMethod("foo");
        am.setAccessible(true);
        final MethodHandle mh = sfmh;

        Bench[] marks = {
            new Bench("reflective invocation (without setAccessible)") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += (Integer) m.invoke(invocationTarget);
                    }
                    return x;
                }
            },
            new Bench("reflective invocation (with setAccessible)") {                   
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += (Integer) am.invoke(invocationTarget);
                    }
                    return x;
                }
            },
            new Bench("methodhandle invocation") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += (Integer) mh.invokeExact(invocationTarget);
                    }
                    return x;
                }
            },
            new Bench("static final methodhandle invocation") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += (Integer) sfmh.invokeExact(invocationTarget);
                    }
                    return x;
                }
            },
            new Bench("direct invocation") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += invocationTarget.foo();
                    }
                    return x;
                }
            },
        };
        for (Bench bm : marks) {
            System.out.println(bm);
        }
    }
}

on my somewhat dated notebook with

java version "1.7.0_02"
Java(TM) SE Runtime Environment (build 1.7.0_02-b13)
Java HotSpot(TM) Client VM (build 22.0-b10, mixed mode, sharing)

this prints:

reflective invocation (without setAccessible)   568.506 ns
reflective invocation (with setAccessible)  42.377 ns
methodhandle invocation 27.461 ns
static final methodhandle invocation    9.402 ns
direct invocation   9.363 ns

Update: As Irreputable points out, the server VM has somewhat different performance characteristics, so using a MethodHandle in a server VM will only help if you can put it in a static final field, in which case the VM can inline the call:

reflective invocation (without setAccessible)   9.736 ns
reflective invocation (with setAccessible)  7.113 ns
methodhandle invocation 26.319 ns
static final methodhandle invocation    0.045 ns
direct invocation   0.044 ns

I recommend that you measure your particular use case.


40

If the static final MethodHandle option discussed above is not practical/possible, another option would be to dynamically generate a class using bytebuddy which has a single method taking foo, invoking the bar method on foo and returning the result.

This would provide performance on individual invocations of bar being essentially the same as direct invocation (the wrapped call would likely eventually be in-lined).

However, this would incur 1 time cost of generating the byte code for class and method. The cost for this is in the neighborhood of 200ns according to bytebuddy site.


6

Calling barReadMethod.setAccessible(true); turns off the security checks which can make it a bit faster. Even if it is accessible, it has to check otherwise.

If I run use a getter method with and without accessible true.

class Main {
    static class A {
        private final Integer i;

        A(Integer i) {
            this.i = i;
        }

        public Integer getI() {
            return i;
        }
    }

    public static void main(String... args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        A[] as = new A[100000];
        for (int i = 0; i < as.length; i++)
            as[i] = new A(i);

        for (int i = 0; i < 5; i++) {
            long time1 = timeSetAccessible(as);
            long time2 = timeNotSetAccessible(as);
            System.out.printf("With setAccessible true %.1f ns, Without setAccessible %.1f ns%n",
                   (double) time1 / as.length, (double) time2 / as.length);
        }
    }

    static long dontOptimiseAvay = 0;

    private static long timeSetAccessible(A[] as) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Method getter = A.class.getDeclaredMethod("getI");
        getter.setAccessible(true);
        dontOptimiseAvay = 0;
        long start = System.nanoTime();
        for (A a : as) {
            dontOptimiseAvay += (Integer) getter.invoke(a);
        }
        return System.nanoTime() - start;
    }

    private static long timeNotSetAccessible(A[] as) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Method getter = A.class.getDeclaredMethod("getI");
//        getter.setAccessible(true);
        dontOptimiseAvay = 0;
        long start = System.nanoTime();
        for (A a : as) {
            dontOptimiseAvay += (Integer) getter.invoke(a);
        }
        return System.nanoTime() - start;
    }
}

prints

With setAccessible true 106.4 ns, Without setAccessible 126.9 ns
With setAccessible true 5.4 ns, Without setAccessible 29.4 ns
With setAccessible true 3.2 ns, Without setAccessible 9.9 ns
With setAccessible true 3.1 ns, Without setAccessible 9.0 ns
With setAccessible true 3.1 ns, Without setAccessible 8.9 ns

For a simple getter, using setAccessible(true) can be three times faster.