أسرع جافا انعكاس البديل

مرحبا بالجميع. اليوم نريد أن نطلعكم على ترجمة لمقال أعد خصيصًا لطلاب دورة Java Developer .

في مقالة " مواصفات النموذج" الخاصة بي ، لم أذكر على وجه التحديد المكون الأساسي الذي ساعد كثيرًا في التنفيذ. سوف أتحدث هنا أكثر عن فئة JavaBeanUtil التي اعتدت الحصول عليها من قيمة حقل كائن. في هذا المثال ، كان FxTransaction .



بالطبع ، ستقول أنه يمكنك استخدام Apache Commons BeanUtils أو أحد بدائلها للحصول على نفس النتيجة. لكنني كنت مهتمًا بالتعمق في هذا الأمر وما تعلمته يعمل بشكل أسرع بكثير من أي مكتبة مبنية على أساس Java Reflection المعروف.

التكنولوجيا التي تتجنب الانعكاس البطيء للغاية هي invokedynamic bytecode invokedynamic . باختصار ، كان مظهر invokedynamic (أو "indy") أخطر ابتكار في Java 7 ، مما مهد الطريق لتنفيذ اللغات الديناميكية على رأس JVM باستخدام استدعاء الأساليب الديناميكية. في وقت لاحق ، في Java 8 ، سمح هذا أيضًا بتعبيرات lambda ومراجع الطريقة ، وكذلك تسلسل السلسلة المحسّن في Java 9.

باختصار ، فإن الأسلوب الذي سأقوم بوصفه أدناه يستخدم LambdaMetafactory و MethodHandle لإنشاء تطبيق لواجهة الوظيفة بشكل حيوي. الوظيفة هي الطريقة الوحيدة التي تفوض استدعاء الأسلوب الفعلي المستهدف برمز محدد داخل لامدا.

في هذه الحالة ، تتمثل الطريقة المستهدفة في إمكانية الوصول المباشر إلى الحقل الذي نريد قراءته. أيضًا ، يجب أن أقول أنه إذا كنت على دراية جيدة بالابتكارات التي ظهرت في Java 8 ، فستجد مقتطفات الكود أدناه بسيطة للغاية. خلاف ذلك ، قد يبدو رمز معقدة للوهلة الأولى.

ألقِ نظرة على JavaBeanUtil المؤقتة


طريقة getFieldValue هي طريقة مساعدة تستخدم لقراءة القيم من حقل JavaBean. يستغرق كائن JavaBean واسم الحقل. يمكن أن يكون اسم الحقل بسيطًا (على سبيل المثال ، fieldA ) أو متداخلًا ، مفصولًا بنقاط (على سبيل المثال ، nestedJavaBean.nestestJavaBean.fieldA ).

 private static final Pattern FIELD_SEPARATOR = Pattern.compile("\\."); private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); private static final ClassValue<Map<String, Function>> CACHE = new ClassValue<Map<String, Function>>() { @Override protected Map<String, Function> computeValue(Class<?> type) { return new ConcurrentHashMap<>(); } }; public static <T> T getFieldValue(Object javaBean, String fieldName) { return (T) getCachedFunction(javaBean.getClass(), fieldName).apply(javaBean); } private static Function getCachedFunction(Class<?> javaBeanClass, String fieldName) { final Function function = CACHE.get(javaBeanClass).get(fieldName); if (function != null) { return function; } return createAndCacheFunction(javaBeanClass, fieldName); } private static Function createAndCacheFunction(Class<?> javaBeanClass, String path) { return cacheAndGetFunction(path, javaBeanClass, createFunctions(javaBeanClass, path) .stream() .reduce(Function::andThen) .orElseThrow(IllegalStateException::new) ); } private static Function cacheAndGetFunction(String path, Class<?> javaBeanClass, Function functionToBeCached) { Function cachedFunction = CACHE.get(javaBeanClass).putIfAbsent(path, functionToBeCached); return cachedFunction != null ? cachedFunction : functionToBeCached; } 


لتحسين الأداء ، أقوم بتخزين وظيفة تم إنشاؤها ديناميكيًا وسوف تقرأ بالفعل قيمة من حقل باسم fieldName . في طريقة getCachedFunction ، كما ترون ، هناك مسار "سريع" يستخدم ClassValue للتخزين المؤقت ومسار createAndCacheFunction "البطيء" ، والذي يتم تنفيذه إذا لم يتم العثور على قيمة في ذاكرة التخزين المؤقت.

createFunctions الأسلوب createFunctions طريقة تُرجع قائمة الدوال التي سيتم ربطها باستخدام Function::andThen . يمكن تمثيل ربط الوظائف مع بعضها البعض في سلسلة getNestedJavaBean().getNestJavaBean().getNestJavaBean().getFieldA() متداخلة ، على غرار getNestedJavaBean().getNestJavaBean().getNestJavaBean().getFieldA() . بعد ذلك ، نضع الوظيفة ببساطة في ذاكرة التخزين المؤقت عن طريق استدعاء طريقة cacheAndGetFunction .
إذا نظرت عن كثب إلى إنشاء الوظيفة ، فسنحتاج إلى استعراض الحقول في path كما يلي:

 private static List<Function> createFunctions(Class<?> javaBeanClass, String path) { List<Function> functions = new ArrayList<>(); Stream.of(FIELD_SEPARATOR.split(path)) .reduce(javaBeanClass, (nestedJavaBeanClass, fieldName) -> { Tuple2<? extends Class, Function> getFunction = createFunction(fieldName, nestedJavaBeanClass); functions.add(getFunction._2); return getFunction._1; }, (previousClass, nextClass) -> nextClass); return functions; } private static Tuple2<? extends Class, Function> createFunction(String fieldName, Class<?> javaBeanClass) { return Stream.of(javaBeanClass.getDeclaredMethods()) .filter(JavaBeanUtil::isGetterMethod) .filter(method -> StringUtils.endsWithIgnoreCase(method.getName(), fieldName)) .map(JavaBeanUtil::createTupleWithReturnTypeAndGetter) .findFirst() .orElseThrow(IllegalStateException::new); } 


createFunctions الأسلوب createFunctions أعلاه لكل حقل createFunctions التي يتم التصريح به طريقة createFunction ، والتي تبحث عن getter المطلوب باستخدام javaBeanClass.getDeclaredMethods() . بمجرد العثور على getter ، يتم تحويله إلى Tuple tuple (Tuple من مكتبة Vavr ) ، والذي يحتوي على النوع الذي تم إرجاعه من getter ، ووظيفة تم إنشاؤها ديناميكيًا والتي سوف تتصرف كما لو كانت getter نفسها.
يتم إنشاء tuple باستخدام أسلوب createTupleWithReturnTypeAndGetter مع طريقة createCallSite كما يلي:

 private static Tuple2<? extends Class, Function> createTupleWithReturnTypeAndGetter(Method getterMethod) { try { return Tuple.of( getterMethod.getReturnType(), (Function) createCallSite(LOOKUP.unreflect(getterMethod)).getTarget().invokeExact() ); } catch (Throwable e) { throw new IllegalArgumentException("Lambda creation failed for getterMethod (" + getterMethod.getName() + ").", e); } } private static CallSite createCallSite(MethodHandle getterMethodHandle) throws LambdaConversionException { return LambdaMetafactory.metafactory(LOOKUP, "apply", MethodType.methodType(Function.class), MethodType.methodType(Object.class, Object.class), getterMethodHandle, getterMethodHandle.type()); } 


في الطريقتين السابقتين ، استخدم ثابتًا يسمى LOOKUP ، وهو مجرد إشارة إلى MethodHandles.Lookup . باستخدامه ، يمكنني إنشاء ارتباط مباشر لطريقة (مقبض الطريقة المباشرة) استنادًا إلى أداة بحث تم العثور عليها مسبقًا. وأخيرًا ، يتم تمرير MethodHandle التي تم إنشاؤها إلى طريقة createCallSite ، حيث يتم إنشاء نص lambda للدالة باستخدام LambdaMetafactory . من هناك ، في النهاية ، يمكننا الحصول على مثيل لـ CallSite ، وهو "الوصي" على الوظيفة.
لاحظ أنه بالنسبة للمقيمين ، يمكنك استخدام نهج مماثل باستخدام BiFunction بدلاً من Function .

المؤشر


لقياس الأداء ، استخدمت أداة JMH الرائعة ( Java Microbenchmark Harness ) ، والتي من المحتمل أن تكون جزءًا من JDK 12 ( ملاحظة المترجم: نعم ، jmh مضمن في java 9 ). كما تعلمون ، فإن النتيجة تعتمد على النظام الأساسي ، لذلك كمرجع: 1x6 i5-8600K 3,6 Linux x86_64, Oracle JDK 8u191 GraalVM EE 1.0.0-rc9 .
للمقارنة ، اخترت مكتبة Apache Commons BeanUtils ، المعروفة على نطاق واسع لمعظم مطوري Java ، وواحدة من بدائلها تسمى Jodd BeanUtil ، والتي يزعم أنها أسرع بنحو 20٪ .

الكود المرجعي هو كما يلي:

 @Fork(3) @Warmup(iterations = 5, time = 3) @Measurement(iterations = 5, time = 1) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Thread) public class JavaBeanUtilBenchmark { @Param({ "fieldA", "nestedJavaBean.fieldA", "nestedJavaBean.nestedJavaBean.fieldA", "nestedJavaBean.nestedJavaBean.nestedJavaBean.fieldA" }) String fieldName; JavaBean javaBean; @Setup public void setup() { NestedJavaBean nestedJavaBean3 = NestedJavaBean.builder().fieldA("nested-3").build(); NestedJavaBean nestedJavaBean2 = NestedJavaBean.builder().fieldA("nested-2").nestedJavaBean(nestedJavaBean3).build(); NestedJavaBean nestedJavaBean1 = NestedJavaBean.builder().fieldA("nested-1").nestedJavaBean(nestedJavaBean2).build(); javaBean = JavaBean.builder().fieldA("fieldA").nestedJavaBean(nestedJavaBean1).build(); } @Benchmark public Object invokeDynamic() { return JavaBeanUtil.getFieldValue(javaBean, fieldName); } /** * Reference: http://commons.apache.org/proper/commons-beanutils/ */ @Benchmark public Object apacheBeanUtils() throws Exception { return PropertyUtils.getNestedProperty(javaBean, fieldName); } /** * Reference: https://jodd.org/beanutil/ */ @Benchmark public Object joddBean() { return BeanUtil.declared.getProperty(javaBean, fieldName); } public static void main(String... args) throws IOException, RunnerException { Main.main(args); } } 


يحدد المعيار أربعة سيناريوهات لمستويات مختلفة من تداخل الحقل. لكل حقل ، ستقوم JMH بإجراء 5 تكرارات مدتها 3 ثوان للتسخين ، ثم 5 تكرارات لمدة ثانية واحدة للقياس الفعلي. سيتم تكرار كل سيناريو 3 مرات للحصول على قياسات أفضل.

النتائج


لنبدأ بالنتائج المترجمة لـ JDK 8u191 :


أوراكل JDK 8u191

أسوأ سيناريو يستخدم الأسلوب invokedynamic هو أسرع بكثير من أسرع المكتبات الأخرى. هذا فرق كبير ، وإذا كنت تشك في النتائج ، يمكنك دائمًا تنزيل التعليمات البرمجية المصدر واللعب بها كما تريد.

الآن دعونا نرى كيف يعمل نفس الاختبار مع GraalVM EE 1.0.0-rc9.


GraalVM EE 1.0.0-rc9

يمكن رؤية النتائج الكاملة هنا باستخدام JMH Visualizer الجميل.

الملاحظات


كان هذا الاختلاف الكبير يرجع إلى حقيقة أن برنامج التحويل البرمجي JIT يعرف CallSite و MethodHandle جيدًا MethodHandle ، على عكس نهج الانعكاس. بالإضافة إلى ذلك ، يمكنك رؤية مدى واعدة GraalVM . يقوم برنامج التحويل البرمجي بعمل رائع حقًا يمكنه تحسين أداء الانعكاس بشكل كبير.

إذا كنت فضوليًا وترغب في الخوض في أعمق ، فإنني أشجعك على انتزاع الكود من مستودع جيثب الخاص بي. ضع في اعتبارك ، لا أنصحك بإنشاء JavaBeanUtil عصامي لاستخدامه في الإنتاج. هدفي هو ببساطة عرض تجربتي والإمكانيات التي يمكننا الحصول عليها من invokedynamic .

انتهت الترجمة ، ونحن ندعو الجميع إلى ندوة مجانية على الإنترنت في 13 يونيو ، والتي سنبحث فيها كيف يمكن أن يكون Docker مفيدًا لمطور Java: كيفية عمل صورة لرسو السفن باستخدام تطبيق java وكيفية التفاعل معها.

Source: https://habr.com/ru/post/ar455122/


All Articles