Привет!
Недавно, путешествуя по коду своего рабочего проекта набрел на довольно высоконагруженный 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. Буду рад комментариям экспертов, которые смогут объяснить те или иные эффекты, повлиявшие на результат.
Автор: SerCe