Производительность вызова метода / Field.getAnnotation (класса) несколько раз по сравнению с предварительным кэшированием этих данных на карте

#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. Предварительное вычисление во время инициализации контекста звучит как достойный выбор, учитывая, что классы будут загружены один раз и больше никогда не изменятся.