Вызывая геттер в Java, хотя и задумался: каков самый быстрый способ его неоднократного вызова (с точки зрения производительности и масштабируемости)?

Учитывая класс Foo и собственность bar, ни один из которых я не знаю во время компиляции, мне нужно повторно вызывать геттер Foo.getBar() много-много раз.

Предположим, у меня есть:

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

И мне нужно сделать что-то вроде этого:

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

Реализация выше все еще очень медленная по сравнению с вызовом без отражения. Есть ли более быстрый способ?

Какой самый быстрый способ вызвать геттер с отражением в Java?

3 ответа

Решение

Вы можете использовать MethodHandle. Его Javadoc пишет:

Используя фабричные методы в API поиска, любой член класса, представленный объектом Core Reflection API, может быть преобразован в поведенчески эквивалентный дескриптор метода. Например, рефлексивный метод можно преобразовать в дескриптор метода с помощью Lookup.unreflect. Получающиеся дескрипторы метода обычно обеспечивают более прямой и эффективный доступ к базовым членам класса.

Хотя это уменьшит накладные расходы, дескрипторы методов по-прежнему будут препятствовать определенным оптимизациям (таким, как встраивание методов), которые JVM может использовать, если вызов выполняется с помощью обычных (неотражающих) инструкций байтового кода. Будет ли такая оптимизация полезной, зависит от того, как вы используете метод (если этот путь к коду всегда вызывает один и тот же метод, может помочь встраивание, если каждый раз это другой метод, вероятно, нет).

Следующий микробенчмарк может дать вам приблизительное представление об относительной производительности отражения, дескрипторов методов и прямого вызова:

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);
        }
    }
}

на моей несколько устаревшей записной книжке с

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)

это печатает:

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

Обновление: Как указывает Irreputable, виртуальная машина сервера имеет несколько иные характеристики производительности, поэтому использование MethodHandle в виртуальной машине сервера поможет только в том случае, если вы можете поместить его в статическое конечное поле, и в этом случае виртуальная машина может встроить вызов:

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

Я рекомендую вам измерить ваш конкретный вариант использования.

Призвание barReadMethod.setAccessible(true); отключает проверки безопасности, которые могут сделать это немного быстрее. Даже если он доступен, он должен проверить иначе.

Если я бегу, использую метод получения с доступным значением 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;
    }
}

печать

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

Для простого метода получения использование setAccessible(true) может быть в три раза быстрее.

Если описанная выше статическая конечная опция MethodHandle не является практичной / невозможной, другой опцией будет динамическое создание класса с использованием bytebuddy, в котором один метод принимает foo, вызывая метод bar для foo и возвращая результат.

Это обеспечило бы производительность на отдельных вызовах bar, по существу, такую ​​же, как и на прямом вызове (завернутый вызов, вероятно, в конечном итоге был бы встроен).

Однако это потребует затрат времени на создание байтового кода для класса и метода. Стоимость для этого составляет около 200 нс в соответствии с сайтом bytebuddy.

Другие вопросы по тегам