بديل أسرع لـ Java Reflection

في مقالة نمط المواصفات ، من أجل سلامة العقل ، لم أذكر عنصرًا أساسيًا لتحقيق ذلك الشيء بشكل جيد. الآن ، سوف أتوسع أكثر قليلاً حول فئة JavaBeanUtil ، التي وضعتها لقراءة قيمة معطى fieldNameمن معين javaBeanObject، والتي تحولت في تلك المناسبة إلى FxTransaction.

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

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

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

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

نظرة خاطفة على JavaBeanUtil محلية الصنع

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

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

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

الحفر أكثر قليلاً في المسار البطيء لإنشاء الوظيفة ، نحتاج إلى التنقل بشكل فردي عبر pathمتغير الحقل عن طريق تقسيمه على النحو التالي:

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

يتم إجراء تخطيط المجموعة هذا createTupleWithReturnTypeAndGetterبالاشتراك مع createCallSiteالطريقة على النحو التالي:

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

لاحظ أنه إذا كنت أرغب في التعامل مع المحددات ، فيمكنني استخدام نهج مماثل من خلال الاستفادة من BiFunction بدلاً من الوظيفة.

المعيار

من أجل قياس مكاسب الأداء ، استخدمت أداة JMH (Java Microbenchmark Harness) الرائعة دائمًا ، والتي من المحتمل أن تكون جزءًا من JDK 12. كما تعلمون ، النتائج مرتبطة بالمنصة ، لذلك كمرجع سوف أن تستخدم واحدة 1x6 i5-8600K 3.6GHzو Linux x86_64وكذلك Oracle JDK 8u191و GraalVM EE 1.0.0-rc9.

للمقارنة ، استخدمت Apache Commons BeanUtils ، وهي مكتبة مشهورة لمعظم مطوري Java ، وأحد بدائلها يسمى Jodd BeanUtil والتي تدعي أنها أسرع بنسبة 20٪ تقريبًا.

يتم تعيين سيناريو المعيار على النحو التالي:

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

النتائج

لنبدأ بالنتائج التي تم جمعها من JDK 8u191السباق:

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

الآن ، دعنا نرى كيف يعمل نفس المعيار مع GraalVM EE 1.0.0-rc9

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

ملاحظات

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

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