Какой самый быстрый способ прочитать геттер из 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 вызовов
- Точно так же, если вы пишете 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, являются их собственными. |