Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Уменьшаем количество рефлексии в коде

Уменьшаем количество рефлексии в коде

Поговорим о том, какие есть альтернативы вызовам методов через рефлексию и почему важно уменьшать количество рефлексивных вызовов в коде. Рассмотрим пример кодогенерации, а также использование механизма LambdaMetafactory. Также сделаем небольшой микробенчмарк, который позволит нам сравнить разные способы вызова метода класса.

Видео: https://youtu.be/O7tEuZihzDA

Deep Refactoring

June 27, 2019
Tweet

More Decks by Deep Refactoring

Other Decks in Education

Transcript

  1. Reflection везде • Dependency Injection • Proxy • Listeners •

    Aspects • Современная Java, скорее всего, не стала бы одним из самых распространенных языков Reflection
  2. Reflection – dark side • Сложнее отладка и поиск ошибки

    • Stacktrace • Типизация • Скорость выполнения уменьшается • AOT компиляция • 60-e в 2020-м • GraalVM • Excelsior (?)
  3. Reflection - что используется? • Большое количество Reflection вызовов –

    работа с метаинформацией • Вызов методов занимает значительную часть в статистике использования Reflection Challenges for Static Analysis of Java Reflection – Literature Review and Empirical Study, Davy Landman, Alexander Serebrenik, Jurgen J. Vinju Centrum Wiskunde & Informatica, Amsterdam, The Netherlands, Eindhoven University of Technology, Eindhoven, The Netherlands
  4. Reflection - как влияет? • Cтатические данные можно кэшировать •

    Вызовы методов все время проверяются JVM @CallerSensitive @ForceInline // to ensure Reflection.getCallerClass optimization @HotSpotIntrinsicCandidate public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, Modifier.isStatic(modifiers) ? null : obj.getClass(), modifiers); } MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); }
  5. Ничего • Пусть JVM отдувается за вас • Можно использовать

    флаги • sun.reflect.inflationThreshold • sun.reflect.noInflation • ReflectionFactory • NativeMethodAccessorImpl • DelegatingMethodAccessorImpl • NativeConstructorAccessorImpl • DelegatingConstructorAccessingImpl
  6. Генерация кода в JVM if (++numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass()))

    { MethodAccessorImpl acc = (MethodAccessorImpl) new MethodAccessorGenerator(). generateMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes(), method.getReturnType(), method.getExceptionTypes(), method.getModifiers()); parent.setDelegate(acc); }
  7. Кодогенерация • Compile-time • Генерация вспомогательных классов (JAXB, QueryDSL) •

    Ресурсы, документация и т.д. • Дополнительный плагин для обработки байткода • Run-time • CGLIB • ByteBuddy • Javassist protected void enhanceSetters(CtClass ctClass) throws NotFoundException, CannotCompileException { for (CtMethod ctMethod : ctClass.getDeclaredMethods()) { ctMethod.addLocalVariable("__prev", setterParamType); ctMethod.addLocalVariable("__new", setterParamType); ctMethod.insertBefore( "__prev = this.get" + StringUtils.capitalize(fieldName) + "();" ); ctMethod.insertAfter( "__new = this.get" + StringUtils.capitalize(fieldName) + "();" + "if (!InstanceUtils.propertyValueEquals(__prev, __new)) {" + " this.propertyChanged(\"" + fieldName + "\", __prev, __new);" + "}" ); } }
  8. Кодогенерация • Код уже не ваш • Отладка по декомпилированным

    исходникам • Другой Stacktrace • Генерация – отдельная задача • Ошибки в генераторе могут дорого стоить • Нужно обновлять больше инструментов
  9. Кодогенерация I took a glance at the ASM generation and

    its pretty incredible. You guys are working with 3 or 4 code abstractions: javax.lang.model.**, ASM, and java.lang.reflect for some edge case. Two of those libraries uses visitor patterns along with your own visitor patters. Consequently it makes it pretty difficult to figure out whats going on. https://github.com/micronaut-projects/micronaut-core/issues/445
  10. Кодогенерация > Task :compileJava FAILED Note: Creating bean classes for

    1 type elements error: Unexpected error: Illegal name .$HelloControllerDefinition 1 error FAILURE: Build failed with an exception. import io.micronaut.http.annotation.*; @Controller("/hello") public class HelloController { @Get public String index() { return "Hello World"; } } HelloController needs to be defined to be in a package.
  11. LambdaMetafactory • Java7 - invokedynamic The Well-Grounded Java Developer. Vital

    techniques of Java 7 and polyglot programming Benjamin J. Evans and Martijn Verburg Foreword by Dr. Heinz Kabutz July 2012
  12. MethodHandle и LambdaMetafactory • Новый API для динамического вызова методов

    • MethodHandle – типизированный указатель на метод • LambdaMetafactory позволяет «обернуть» вызов метода • Связка с методом делается один раз • Возможен inline кода лямбды
  13. LambdaMetafactory @FunctionalInterface interface PropertyChangeListener { void propertyChanged(PropertyChangeEvent e); } @Autowired

    EventProcessor bean; … Instance.PropertyChangeListener l = (e) -> {bean.processEvent(e);} … l.propertyChanged(event);
  14. LambdaMetafactory MethodHandles.Lookup caller = MethodHandles.lookup(); CallSite site = LambdaMetafactory.metafactory(caller, "apply",

    MethodType.methodType(BiFunction.class), MethodType.methodType(Object.class, Object.class, Object.class), caller.findVirtual( bean.getClass(), method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()[0])), MethodType.methodType(Object.class, bean.getClass(), method.getParameterTypes()[0])); MethodHandle factory = site.getTarget(); BiFunction listenerMethod = (BiFunction) factory.invoke();
  15. Преимущества LambdaMetafactory • Код проекта не меняется • Stacktrace –

    короче • В теории – должно быть быстрее public class ApplicationListenerMethodAdapter implements GenericApplicationListener { private final Method method; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = this.method.invoke(bean, event); handleResult(result); } } public class ApplicationListenerLambdaAdapter extends ApplicationListenerMethodAdapter { private final BiFunction funHandler; public void onApplicationEvent(ApplicationEvent event) { Object bean = getTargetBean(); Object result = funHandler.apply(bean, event); handleResult(result); } }
  16. Проверяем теорию практикой • Будем использовать JMH • Ограничимся простыми

    вызовами get/set @Benchmark @BenchmarkMode({Mode.Throughput, Mode.AverageTime}) @Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS) public void nativeSetTest(){ user.setValue("name", "someName"); } @Benchmark @BenchmarkMode({Mode.Throughput, Mode.AverageTime}) @Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS) public Object nativeGetTest(){ return user.getValue("name"); }
  17. Проверяем теорию практикой public class User { private final MethodsCache

    methodCache; private final LambdaMethodsCache lambdaMethodsCache; private final MethodHandleCache methodHandleCache; private UUID id; private String name; public void setValue(String propertyName, Object propertyValue) { setValueNative(propertyName, propertyValue); } public Object getValue(String propertyName) { return getValueNative(propertyName); } //Classic reflection method access public void setValueReflectionCache(String propertyName, Object propertyValue) { methodCache.setValue(propertyName, propertyValue, this); } public Object getValueReflectionCache(String propertyName) { return methodCache.getValue(propertyName, this); } } public void setValueNative(String propertyName, Object propertyValue) { switch (propertyName) { case "name": this.setName((String) propertyValue); return; case "id": this.setId((UUID) propertyValue); return; default: { throw new RuntimeException()); } } }
  18. Проверяем теорию практикой public abstract class AbstractMethodsCache<G, S> { protected

    final Map<String, G> getters = new HashMap<>(); protected final Map<String, S> setters = new HashMap<>(); protected abstract G createGetter(Class clazz, Method method); protected abstract S createSetter(Class clazz, Method method); public abstract Object getValue(String property, Object object); public abstract void setValue(String property, Object value, Object object); } public class LambdaMethodsCache extends AbstractMethodsCache<Function, BiConsumer>{ }
  19. Проверяем теорию практикой GetValue Throughput (ops/us) Execution Time (us/op) lambdaCacheGetTest

    55 0.018 reflectionCacheGetTest 48 0.017 methodHandleCacheGetTest 33 0.019 nativeGetTest 160 0.004 SetValue Throughput (ops/us) Execution Time (us/op) lambdaCacheSetTest 67 0.013 reflectionCacheSetTest 59 0.025 methodHandleCacheSetTest 42 0.029 nativeSetTest 355 0.002
  20. Итого • Reflection будет жить ещё долго • Слишком много

    всего завязано на этом • Кодогенерация • Похоже, будет развиваться • AOT компиляция, serverless и прочее в тренде • LambdaMetafactory • Если пишете свой фреймворк и не хотите кодогенерацию и AOT • Не сложнее и быстрее Reflection • Использование MethodHandle не дает прироста производительности • Связывание с методом – долгая процедура • Можно попробовать статические MethodHandle • А можно просто пить пиво и надеяться на JVM