Подтвердить что ты не робот

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

Используя методы factory в API Lookup, любой член класса, представленный объектом API Core Reflection, может быть преобразован в обработчик поведенческого эквивалентного метода. Например, отражающий метод может быть преобразован в дескриптор метода с помощью 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

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

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

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

Ответ 2

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

Если я использую метод getter с и без доступных 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

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

Ответ 3

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

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

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