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

в 19:30, , рубрики: Без рубрики

Привет!

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

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

С быстрой руки был написан микробенчмарк на 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:
Сравниваем производительность reflection в JDK8 и JDK7

Результаты для JDK8:
Сравниваем производительность reflection в JDK8 и JDK7

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

Собственно, результаты вполне ожидаемы:

  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:
Сравниваем производительность reflection в JDK8 и JDK7

Результаты для JDK8:
Сравниваем производительность reflection в JDK8 и JDK7

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

Из графиков можно сделать несколько выводов:

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

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

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

P.S. Буду рад комментариям экспертов, которые смогут объяснить те или иные эффекты, повлиявшие на результат.

Автор: SerCe

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js