#java #performance #reflection #map #annotations
#java #Производительность #отражение #словарь #примечания
Вопрос:
Я хотел бы знать, есть ли какие-либо сравнения / исследования производительности повторного вызова (в Java) методов Method.getAnnotation(Class)
и Field.getAnnotation(Class)
по сравнению с сохранением (во время запуска программы) предварительно вычисленной карты с этой информацией метаданных классов и повторным запросом к ней позже. Какой из них обеспечит наилучшую производительность во время выполнения?
И эта производительность была бы одинаковой под Java 5, 6 и 7?
Ответ №1:
Карта должна быть более предпочтительным подходом. Основная проблема заключается не только в кэшировании. Но также улучшает многопоточность. В методе.getAnnotation() он вызывает синхронизированный частный метод declaredAnnotations(). Синхронизированный метод плохо влияет на многопоточное приложение.
Комментарии:
1. В многопоточном приложении Java EE, где
getAnnotation
выполнялся вызов 18 тыс. раз, измеренное время работы на стенде сократилось с 3,7 с до 0,09 с при использованииConcurrentHasMap
для кэширования.
Ответ №2:
Я знаю, что это довольно старый вопрос, но результат для более поздних JDK все еще может представлять интерес.
Я написал некоторый тест JMH, чтобы получить представление о том, какое влияние может оказать кэширование информации аннотации:
@State(Scope.Thread)
public static class StateWithMethodAndHashMap {
public StateWithMethodAndHashMap() {
try {
cache = new HashMap<>();
method = AnnotationCachingBenchmark.class.getMethod("methodWithAnnotations");
final Annotation annotation = method.getAnnotation(Deprecated.class);
cache.put(method, annotation);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
final HashMap<Method, Annotation> cache;
final Method method;
}
@State(Scope.Thread)
public static class StateWithMethodAndConcurrentHashMap {
public StateWithMethodAndConcurrentHashMap() {
try {
cache = new ConcurrentHashMap<>();
method = AnnotationCachingBenchmark.class.getMethod("methodWithAnnotations");
cache.put(method, method.getAnnotation(Deprecated.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
final ConcurrentHashMap<Method, Annotation> cache;
final Method method;
}
@State(Scope.Thread)
public static class StateWithMethod {
public StateWithMethod() {
try {
method = AnnotationCachingBenchmark.class.getMethod("methodWithAnnotations");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
final Method method;
}
@Deprecated
public void methodWithAnnotations() {
}
@Benchmark
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void annotationsByReflection(final Blackhole aBh, final StateWithMethod aState) throws Exception {
aBh.consume(aState.method.isAnnotationPresent(Deprecated.class)
|| aState.method.getClass().isAnnotationPresent(Deprecated.class));
}
@Benchmark
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void annotationsByHashMap(final Blackhole aBh, final StateWithMethodAndHashMap aState) throws Exception {
aBh.consume(aState.cache.get(aState.method));
}
@Benchmark
@BenchmarkMode(Mode.All)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void annotationsByConcurrentHashMap(final Blackhole aBh, final StateWithMethodAndConcurrentHashMap aState)
throws Exception {
aBh.consume(aState.cache.get(aState.method));
}
JDK 1.8.0_172, 64-разрядная серверная виртуальная машина Java HotSpot (TM), 25.172-b11:
Benchmark Mode Cnt Score Error Units
AnnotationCachingBenchmark.annotationsByConcurrentHashMap thrpt 5 0.152 ± 0.009 ops/ns
AnnotationCachingBenchmark.annotationsByHashMap thrpt 5 0.144 ± 0.005 ops/ns
AnnotationCachingBenchmark.annotationsByReflection thrpt 5 0.043 ± 0.001 ops/ns
AnnotationCachingBenchmark.annotationsByConcurrentHashMap avgt 5 6.610 ± 0.094 ns/op
AnnotationCachingBenchmark.annotationsByHashMap avgt 5 6.963 ± 0.414 ns/op
AnnotationCachingBenchmark.annotationsByReflection avgt 5 23.248 ± 0.339 ns/op
JDK 13, 64-разрядная серверная виртуальная машина OpenJDK, 13 33:
Benchmark Mode Cnt Score Error Units
AnnotationCachingBenchmark.annotationsByConcurrentHashMap thrpt 5 0.128 ± 0.027 ops/ns
AnnotationCachingBenchmark.annotationsByHashMap thrpt 5 0.136 ± 0.031 ops/ns
AnnotationCachingBenchmark.annotationsByReflection thrpt 5 0.139 ± 0.010 ops/ns
AnnotationCachingBenchmark.annotationsByConcurrentHashMap avgt 5 7.335 ± 1.067 ns/op
AnnotationCachingBenchmark.annotationsByHashMap avgt 5 6.634 ± 0.184 ns/op
AnnotationCachingBenchmark.annotationsByReflection avgt 5 7.234 ± 0.567 ns/op
Вы можете ясно видеть эффект кэширования аннотаций JDK при сравнении 1.8 с 13. Таким образом, с недавними JDK нет необходимости заботиться о кэшировании этой информации, поскольку это привело бы только к дополнительным накладным расходам.
Комментарии:
1. Спасибо за обновление и тесты! Похоже, что синхронизированные блоки в коде отражения были изменены на операции CAS без блокировки, что помогает и делает старые ответы здесь немного менее актуальными для новых JVM.
2. Интересно, имеет ли это место в последних сборках AdoptOpenJDK / Temurin / RedHat Java 8. Нужно проверить это позже.
Ответ №3:
Я думаю, это немного зависит от реализации JVM. Но, взяв пример Oracle JVM, он поддерживает кэш всех аннотаций к экземплярам метода и поля, что эквивалентно подходу map, о котором вы говорите.
Но здесь есть загвоздка; поскольку экземпляры метода / поля уникальны для каждого объекта, в случае, если вы в конечном итоге создадите много объектов для данного класса, вы в значительной степени потеряете предлагаемое преимущество в производительности. В этом случае статическое сопоставление имени класса имени метода / class-name имени поля с соответствующим списком аннотаций превосходит используемый встроенный подход кэширования.
Кстати, как вы предварительно вычисляете карту? Выполняется ли это при запуске приложения или какой-то автоматически сгенерированный код? Вы действительно подтвердили, что в вашем случае безопасно кэшировать экземпляры аннотаций?
Как всегда, для подобных вопросов лучшим решением является профилирование / измерение с помощью вашего приложения в режиме онлайн и выбор решения, которое выглядит как win в данном случае использования.
Комментарии:
1. Немного контекста: это будет частью веб-фреймворка MVC, в частности, аннотации будут прикреплены к классам действий, их методам и полям, поэтому ожидается, что за время существования приложения будет создано множество новых экземпляров. На самом деле я сделал это, используя прямой подход getAnnotation (), но, думаю, я мог бы предварительно вычислить карту при инициализации контекста. на данный момент во фреймворке нет автоматически сгенерированного кода.
2. Предварительное вычисление во время инициализации контекста звучит как достойный выбор, учитывая, что классы будут загружены один раз и больше никогда не изменятся.