Статьи

Отражение Java, но намного быстрее

Какой самый быстрый способ прочитать геттер из Java-класса, не зная этого класса во время компиляции? Фреймворки Java часто делают это. Много. И это может напрямую влиять на их производительность. Итак, давайте оценим различные подходы, такие как отражение, дескрипторы методов и генерация кода.

Вариант использования

Предположим, у нас есть простой класс Person с именем и адресом:

1
2
3
4
5
6
7
public class Person {
   ...
 
   public String getName() {...}
   public Address getAddress() {...}
 
}

и мы хотим использовать такие фреймворки, как:

  • XStream , JAXB или Jackson для сериализации экземпляров в XML или JSON.
  • JPA / Hibernate для хранения лиц в базе данных.
  • OptaPlanner для назначения адресов (если они туристы или бездомные).

Ни одна из этих платформ не знает класс Person . Поэтому они не могут просто вызвать person.getName() :

1
2
3
4
5
// Framework code
   public Object executeGetter(Object object) {
      // Compilation error: class Person is unknown to the framework
      return ((Person) object).getName();
   }

Вместо этого код использует отражение, дескрипторы методов или генерацию кода.

Но такой код называется очень много :

  • Если вы добавите 1000 разных людей в базу данных, JPA / Hibernate, вероятно, вызовет такой код 2000 раз:
    • 1000 вызовов Person.getName()
    • еще 1000 звонков в Person.getAddress()
  • Точно так же, если вы пишете 1000 разных людей в XML или JSON, скорее всего, будет 2000 вызовов XStream, JAXB или Jackson.

Очевидно, что когда такой код вызывается x раз в секунду, его производительность имеет значение .

Тесты

Используя JMH, я запустил ряд микро-тестов, используя OpenJDK 1.8.0_111 в Linux на 64-битном 8-ядерном настольном компьютере Intel i7-4790 с 32 ГБ оперативной памяти. Тест JMH проходил с 3 вилками, 5 итерациями разогрева по 1 секунде и 20 итерациями измерения по 1 секунде.

Исходный код этого теста находится в этом репозитории GitHub .

Результаты TL; DR

  • Отражение Java медленно. (*)
  • Java MethodHandles тоже медленный. (*)
  • Сгенерированный код с javax.tools быстро. (*)

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

Итак, дьявол кроется в деталях. Давайте пройдемся по реализациям, чтобы подтвердить, что я применил типичные магические приемы (такие как setAccessible(true) ).

Реализации

Прямой доступ (базовый уровень)

Я использовал нормальный person.getName() в качестве базовой линии:

1
2
3
4
5
6
7
public final class MyAccessor {
 
    public Object executeGetter(Object object) {
        return ((Person) object).getName();
    }
 
}

Это занимает около 2,7 наносекунд на операцию:

1
2
3
Benchmark           Mode  Cnt  Score   Error  Units
===================================================
DirectAccess        avgt   60  2.667 ± 0.028  ns/op

Прямой доступ, естественно, самый быстрый подход во время выполнения, без затрат на загрузку. Но он импортирует Person во время компиляции, поэтому он непригоден для всех сред.

отражение

Очевидный способ чтения фреймворка, который получает getter во время выполнения, не зная об этом заранее, — через Java Reflection:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public final class MyAccessor {
 
    private final Method getterMethod;
 
    public MyAccessor() {
        getterMethod = Person.class.getMethod("getName");
        // Skip Java language access checking during executeGetter()
        getterMethod.setAccessible(true);
    }
 
    public Object executeGetter(Object bean) {
        return getterMethod.invoke(bean);
    }
 
}

Добавление вызова setAccessible(true) ускоряет эти вызовы отражения, но даже тогда это занимает 5,5 наносекунд на вызов.

1
2
3
4
Benchmark           Mode  Cnt  Score   Error  Units
===================================================
DirectAccess        avgt   60  2.667 ± 0.028  ns/op
Reflection          avgt   60  5.511 ± 0.081  ns/op

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

Это не было для меня большим сюрпризом, потому что, когда я описываю (используя выборку) искусственно простую задачу коммивояжера с 980 городами в OptaPlanner , стоимость отражения выскакивает, как больной большой палец:

MethodHandles

MethodHandle был введен в Java 7 для поддержки invokedynamic инструкций. Согласно javadoc, это типизированная, непосредственно исполняемая ссылка на базовый метод. Звучит быстро, верно?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public final class MyAccessor {
 
    private final MethodHandle getterMethodHandle;
 
    public MyAccessor() {
        MethodHandle temp = lookup.findVirtual(Person.class, "getName", MethodType.methodType(String.class));
        temp = temp.asType(temp.type().changeParameterType(0 , Object.class));
        getterMethodHandle = temp.asType(temp.type().changeReturnType(Object.class));
    }
 
    public Object executeGetter(Object bean) {
        return getterMethodHandle.invokeExact(bean);
    }
 
}

К сожалению, MethodHandle даже медленнее, чем отражение в OpenJDK 8. Он занимает 6,1 наносекунды на операцию, что на 132% медленнее, чем прямой доступ.

1
2
3
4
5
6
Benchmark           Mode  Cnt  Score   Error  Units
===================================================
DirectAccess        avgt   60  2.667 ± 0.028  ns/op
Reflection          avgt   60  5.511 ± 0.081  ns/op
MethodHandle        avgt   60  6.188 ± 0.059  ns/op
StaticMethodHandle  avgt   60  5.481 ± 0.069  ns/op

При этом, если MethodHandle находится в статическом поле, он занимает всего 5,5 наносекунд на операцию, что все еще медленнее, чем отражение . Кроме того, это непригодно для большинства фреймворков. Например, реализация JPA может нуждаться в отражении по m получателям ( Person , Company , Order ,…) m ( getName() , getAddress() , getBirthDate() ,…), так как же реализация JPA иметь n * m статических полей, не зная n ни m во время компиляции?

Я действительно надеюсь, что MethodHandle станет таким же быстрым, как и прямой доступ в будущих версиях Java, заменив необходимость в…

Сгенерированный код с помощью javax.tools.JavaCompiler

В Java можно скомпилировать и запустить сгенерированный код Java во время выполнения. Поэтому с javax.tools.JavaCompiler API javax.tools.JavaCompiler мы можем генерировать код прямого доступа во время выполнения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public abstract class MyAccessor {
 
    public static MyAccessor generate() {
        final String String fullClassName = "x.y.generated.MyAccessorPerson$getName";
        final String source = "package x.y.generated;\n"
                + "public final class MyAccessorPerson$getName extends MyAccessor {\n"
                + "    public Object executeGetter(Object bean) {\n"
                + "        return ((Person) object).getName();\n"
                + "    }\n"
                + "}";
        JavaFileObject fileObject = new ...(fullClassName, source);
 
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        ClassLoader classLoader = ...;
        JavaFileManager javaFileManager = new ...(..., classLoader)
        CompilationTask task = compiler.getTask(..., javaFileManager, ..., singletonList(fileObject));
        boolean success = task.call();
        ...
        Class compiledClass = classLoader.loadClass(fullClassName);
        return compiledClass.newInstance();
    }
 
    // Implemented by the generated subclass
    public abstract Object executeGetter(Object object);
 
}

Для получения дополнительной информации о том, как использовать javax.tools.JavaCompiler , взгляните на страницу 2 этой статьи или этой статьи . Помимо javax.tools , аналогичные подходы могут использовать ASM или CGLIB, но они выводят дополнительные зависимости и могут иметь разные результаты производительности.

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

1
2
3
4
Benchmark           Mode  Cnt  Score   Error  Units
===================================================
DirectAccess        avgt   60  2.667 ± 0.028  ns/op
GeneratedCode       avgt   60  2.745 ± 0.025  ns/op

Поэтому, когда я снова запустил точно такую ​​же задачу коммивояжера в OptaPlanner , на этот раз с использованием генерации кода для доступа к переменным планирования скорость вычисления баллов в целом была на 18% выше . И профилирование (с использованием выборки) тоже выглядит намного лучше:

Обратите внимание, что в нормальных случаях использования такой прирост производительности вряд ли будет обнаружен из-за огромных потребностей ЦП в реально сложном вычислении оценки…

Единственный недостаток генерации кода во время выполнения — это то, что он приводит к заметной стоимости начальной загрузки, особенно если сгенерированный код не компилируется массово. Поэтому я все еще надеюсь, что когда-нибудь MethodHandles получат такой же быстрый доступ, как и прямой доступ, просто чтобы избежать затрат на загрузку.

Вывод

В этом тесте рефлексия и MethodHandles в два раза медленнее, чем прямой доступ в OpenJDK 8, но сгенерированный код быстрее, чем прямой доступ.

1
2
3
4
5
6
7
Benchmark           Mode  Cnt  Score   Error  Units
===================================================
DirectAccess        avgt   60  2.667 ± 0.028  ns/op
Reflection          avgt   60  5.511 ± 0.081  ns/op
MethodHandle        avgt   60  6.188 ± 0.059  ns/op
StaticMethodHandle  avgt   60  5.481 ± 0.069  ns/op
GeneratedCode       avgt   60  2.745 ± 0.025  ns/op
Смотрите оригинальную статью здесь: Java Reflection, но гораздо быстрее

Мнения, высказанные участниками Java Code Geeks, являются их собственными.