Как стать автором
Обновить

Сравниваем производительность reflection в JDK8 и JDK7

Время на прочтение 6 мин
Количество просмотров 23K
Привет, Хабр!

Недавно, путешествуя по коду своего рабочего проекта набрел на довольно высоконагруженный 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:


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



Собственно, результаты вполне ожидаемы:
  1. Проставление setAccessible(true) дает нам прирост производительности за счет отсутствия необходимости проверки прав
  2. Доступ к полям объекта напрямую примерно в 2 раза быстрее доступа через reflection
  3. Интересно что в 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:


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



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


Собственно вывод простой если вы используете в своем проекте reflection — то вот вам лишний повод для перехода на jdk8.

Если вы хотите поиграть с бенчмарком, измерить производительность reflection на своей архитектуре или поискать ошибки то добро пожаловать на github.

P.S. Буду рад комментариям экспертов, которые смогут объяснить те или иные эффекты, повлиявшие на результат.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Используете ли вы reflection в своих проектах?
45.66% Да, reflection 247
7.21% Да, reflection и MethodHandle 39
2.4% Только MethodHandle 13
44.73% Нет 242
Проголосовал 541 пользователь. Воздержались 330 пользователей.
Теги:
Хабы:
+29
Комментарии 13
Комментарии Комментарии 13

Публикации

Истории

Работа

Java разработчик
343 вакансии

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн