Недавно, путешествуя по коду своего рабочего проекта набрел на довольно высоконагруженный spring бин, который производил обращения к методам объектов (иногда и объектов сгенерированных на лету классов) вызывая геттеры и сеттеры объекта через reflection. В бине уже был реализован кэш геттеров, однако я задался вопросом — насколько быстр reflection и можно ли сделать быстрее.

С быстрой руки был написан микробенчмарк на JMH, который меряет производительность различных способов вызова методов. Процесс написания микробенчмарка — дело неблагодарное, существует миллион способов ошибиться и измерить совсем не то, что хотел. Так я упустил из головы боксинг-анбоксинг и в результате в первой версии бенчмарка измерял его, а не сам вызов метода. А ошибку свою нашел, только когда посмотрел PrintAssembly.
Результаты получились интересными, однако на хабре уже были статьи, сравнивающие вызов методов через reflection и напрямую, поэтому, посмотрев на результаты, собрался убрать их в ящик до лучших времен, но внезапно ленту твиттера, заполненную политикой, разбавили твиты про релиз java8. Обуздав радость, я решил сравнить производительность reflection в JDK7 и JDK8.
Кратко про обозначения в результатах фреймворка для правильного бенчмаркинга JMH:
- Benchmark — имя метода, помеченного @GenerateMicroBenchmark
- Mode — режим бенчмарка, в моем случае thrpt — Throughput, количество операций за определенный промежуток времени — в моем случае 1s
- Samples — количество измерений
- Mean — среднее количество выполненных операций за указанный промежуток времени
- Mean error — cтандартная ошибка
- Units — единица измерения — в моем случае операций/секунду
Первое, что я замерил это доступ к полям класса напрямую:
- testFieldSaveAccessible — досуп к полю с вызовом setAccessible(true) на Field
- testFieldSaveNotAccessible — просто доступ через поднятый Field
- testFieldStraighforward — прямой доступ через вызов метода
Аналогично для статических полей.
@BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @State(Scope.Thread) public class ReflectionFieldAccess { private static final Class<TestedClass> clazz = TestedClass.class; private TestedClass testedObject; Field simpleField; Field fieldAccessible; @Setup public void init() { try { testedObject = new TestedClass(); simpleField = clazz.getField("a"); Field Field = clazz.getField("b"); Field.setAccessible(true); fieldAccessible = Field; } catch (Exception e) { // do nothing } } @GenerateMicroBenchmark public Object testFieldSaveAccessible() throws Exception { return fieldAccessible.get(testedObject); } @GenerateMicroBenchmark public Object testFieldSaveNotAccessible() throws Exception { return simpleField.get(testedObject); } @GenerateMicroBenchmark public Object testFieldStraighforward() throws Exception { return testedObject.c; } }
@BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @State(Scope.Thread) public class ReflectionFieldStaticAccess { private static final Class<TestedClass> clazz = TestedClass.class; Field simpleField; Field fieldAccessible; @Setup public void init() { try { simpleField = clazz.getField("aStat"); Field Field = clazz.getField("bStat"); Field.setAccessible(true); fieldAccessible = Field; } catch (Exception e) { // do nothing } } @GenerateMicroBenchmark public Object testFieldSaveAccessible() throws Exception { return fieldAccessible.get(null); } @GenerateMicroBenchmark public Object testFieldSaveNotAccessible() throws Exception { return simpleField.get(null); } @GenerateMicroBenchmark public Object testFieldStraighforward() throws Exception { return TestedClass.cStat; } }
Результаты для JDK7:

Результаты для JDK8:

Результаты в сравнении:


Собственно, результаты вполне ожидаемы:
- Проставление setAccessible(true) дает нам прирост производительности за счет отсутствия необходимости проверки прав
- Доступ к полям объекта напрямую примерно в 2 раза быстрее доступа через reflection
- Интересно что в jdk8 улучшена производительность доступа через reflection
Перейдем к сравнению результатов для вызовов методов, здесь у нас гораздо больший выбор исследуемых средств.
Последние два теста на использование API MethodHandle, часть JSR 292, доступного c jdk7.
- testFastMethod — вызов метода с использованием FastMethod из CGLIB
- testMethodNotAccessible — простой вызов через reflection
- testMethodAccessible — вызов через reflection с вызовом setAccessible(true) на Method
- testMethodHandle — вызов MethodHandle.invoke
- testMethodHandleExact — вызов MethodHandle.invokeExact, требующем точного совпадения типов
Аналогично для статических методов.
@BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @State(Scope.Thread) public class ReflectionMethodAccess { private static final Class<TestedClass> clazz = TestedClass.class; private TestedClass testedObject; Method simpleMethod; Method methodAccessible; FastMethod fastMethod; MethodHandle methodHandle; @Setup public void init() { try { testedObject = new TestedClass(); simpleMethod = clazz.getMethod("getA", null); Method method = clazz.getMethod("getB", null); method.setAccessible(true); methodAccessible = method; fastMethod = FastClass.create(clazz).getMethod("getC", null); methodHandle = MethodHandles.lookup().findVirtual(clazz, "getD", MethodType.methodType(Integer.class)); } catch (Exception e) { // do nothing } } @GenerateMicroBenchmark public Object testFastMethod() throws Throwable { return fastMethod.invoke(testedObject, null); } @GenerateMicroBenchmark public Object testMethodAccessible() throws Throwable { return methodAccessible.invoke(testedObject, null); } @GenerateMicroBenchmark public Object testMethodNotAccessible() throws Throwable { return simpleMethod.invoke(testedObject, null); } @GenerateMicroBenchmark public Object testMethodHandleExact() throws Throwable { return (Integer)methodHandle.invokeExact(testedObject); } @GenerateMicroBenchmark public Object testMethodHandle() throws Throwable { return (Integer)methodHandle.invoke(testedObject); } @GenerateMicroBenchmark public Object testMethodDirect() throws Throwable { return testedObject.getA(); } }
@BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @State(Scope.Thread) public class ReflectionMethodStaticAccess { private static final Class<TestedClass> clazz = TestedClass.class; Method simpleMethod; Method methodAccessible; MethodHandle methodHandle; FastMethod fastMethod; @Setup public void init() { try { simpleMethod = clazz.getMethod("getAStatic", null); Method method = clazz.getMethod("getBStatic", null); method.setAccessible(true); methodAccessible = method; fastMethod = FastClass.create(clazz).getMethod("getCStatic", null); methodHandle = MethodHandles.lookup().findStatic(clazz, "getDStatic", MethodType.methodType(Integer.class)); } catch (Exception e) { // do nothing } } @GenerateMicroBenchmark public Object testFastMethod() throws Throwable { return fastMethod.invoke(null, null); } @GenerateMicroBenchmark public Object testMethodAccessible() throws Throwable { return methodAccessible.invoke(null, null); } @GenerateMicroBenchmark public Object testMethodNotAccessible() throws Throwable { return simpleMethod.invoke(null, null); } @GenerateMicroBenchmark public Object testMethodHandleExact() throws Throwable { return (Integer)methodHandle.invokeExact(); } @GenerateMicroBenchmark public Object testMethodHandle() throws Throwable { return (Integer)methodHandle.invoke(); } @GenerateMicroBenchmark public Object testMethodDirect() throws Throwable { return TestedClass.getAStatic(); } }
Подробнее про MethodHandle можно послушать, например, в докладе Владимира Иванова про invokedynamics
Результаты для JDK7:

Результаты для JDK8:

Результаты в сравнении:


Из графиков можно сделать несколько выводов:
- По неизвестным мне причинам FastMethod для статических методов работал медленно на jdk7, на jdk8 же он работает в 2 раза быстрее — аналогично методу с setAccessible(true) (разница в рамках погрешности)
- В jdk8 очень сильно оптимизирована работа MethodHandle.invoke, наверняка это связано с лямбдами
- Общая производительность reflection выросла, как и для случая с полями
Собственно вывод простой если вы используете в своем проекте reflection — то вот вам лишний повод для перехода на jdk8.
Если вы хотите поиграть с бенчмарком, измерить производительность reflection на своей архитектуре или поискать ошибки то добро пожаловать на github.
P.S. Буду рад комментариям экспертов, которые смогут объяснить те или иные эффекты, повлиявшие на результат.

