Μια ταχύτερη εναλλακτική λύση στο Java Reflection

Στο άρθρο Προδιαγραφή μοτίβου, για λόγους λογικής, δεν ανέφερα για ένα υποκείμενο συστατικό για να κάνω αυτό το πράγμα να συμβεί. Τώρα, θα επεξεργαστώ λίγο περισσότερο στην τάξη JavaBeanUtil, που έβαλα σε εφαρμογή για να διαβάσω την τιμή για ένα δεδομένο fieldNameαπό ένα συγκεκριμένο javaBeanObject, το οποίο σε αυτήν την περίπτωση αποδείχθηκε FxTransaction.

Μπορείτε εύκολα να υποστηρίξετε ότι θα μπορούσα βασικά να χρησιμοποιήσω το Apache Commons BeanUtils ή μία από τις εναλλακτικές του για να επιτύχω το ίδιο αποτέλεσμα. Αλλά με ενδιέφερε να βρώσω τα χέρια μου βρώμικα με κάτι διαφορετικό που ήξερα ότι θα ήταν πολύ πιο γρήγορα από οποιαδήποτε βιβλιοθήκη χτισμένη πάνω από το ευρέως γνωστό Java Reflection.

Το εργαλείο που επιτρέπει την αποφυγή της πολύ αργής αντανάκλασης είναι το invokedynamicοδηγίες bytecode. Εν συντομία, invokedynamic(ή "indy") ήταν το καλύτερο πράγμα που εισήχθη στο Java 7 προκειμένου να ανοίξει ο δρόμος για την εφαρμογή δυναμικών γλωσσών στην κορυφή του JVM μέσω δυναμικής επίκλησης μεθόδου. Αργότερα επέτρεψε επίσης την έκφραση λάμδα και την αναφορά μεθόδου στην Java 8 καθώς και τη συνένωση συμβολοσειρών στην Java 9 να επωφεληθούν από αυτήν.

Με λίγα λόγια, η τεχνική που πρόκειται να περιγράψω καλύτερα παρακάτω αξιοποιεί το LambdaMetafactory και το MethodHandle προκειμένου να δημιουργήσει δυναμικά μια εφαρμογή της Λειτουργίας. Η απλή μέθοδος εκχωρεί μια κλήση στην πραγματική μέθοδο στόχου με έναν κωδικό που ορίζεται στο εσωτερικό του σώματος λάμδα.

Η εν λόγω μέθοδος στόχου εδώ είναι η πραγματική μέθοδος λήψης που έχει άμεση πρόσβαση στο πεδίο που θέλουμε να διαβάσουμε. Επίσης, θα έπρεπε να πω αν είστε αρκετά εξοικειωμένοι με τα ωραία πράγματα που εμφανίστηκαν στο Java 8, θα βρείτε τα παρακάτω αποσπάσματα κώδικα αρκετά εύκολα κατανοητά. Διαφορετικά, μπορεί να είναι δύσκολο με μια ματιά.

Μια ματιά στο σπιτικό JavaBeanUtil

Η ακόλουθη μέθοδος είναι το βοηθητικό πρόγραμμα που χρησιμοποιείται για την ανάγνωση μιας τιμής από ένα πεδίο JavaBean. Παίρνει το αντικείμενο JavaBean και ένα fieldAή ακόμη και ένθετο πεδίο διαχωρισμένα με τελείες, για παράδειγμα,nestedJavaBean.nestedJavaBean.fieldA

Για βέλτιστη απόδοση, αποθηκεύω προσωρινά τη δυναμικά δημιουργημένη λειτουργία που είναι ο πραγματικός τρόπος ανάγνωσης του περιεχομένου ενός δεδομένου fieldName. Έτσι, μέσα στη getCachedFunctionμέθοδο, όπως μπορείτε να δείτε παραπάνω, υπάρχει μια γρήγορη διαδρομή που αξιοποιεί την ClassValue για προσωρινή αποθήκευση και υπάρχει η αργή createAndCacheFunctionδιαδρομή που εκτελείται μόνο αν δεν έχει γίνει κρυφή μνήμη μέχρι στιγμής.

Η αργή διαδρομή βασικά θα ανατεθεί στη createFunctionsμέθοδο που επιστρέφει μια λίστα συναρτήσεων που πρέπει να μειωθούν με την αλυσίδα τους χρησιμοποιώντας Function::andThen. Όταν οι λειτουργίες είναι αλυσοδεμένες, μπορείτε να φανταστείτε ένα είδος ένθετων κλήσεων όπως getNestedJavaBean().getNestedJavaBean().getFieldA(). Τέλος, μετά την αλυσίδα, απλώς θέσαμε τη μειωμένη λειτουργία στη cacheAndGetFunctionμέθοδο κλήσης cache

Εξετάζοντας λίγο περισσότερο την αργή διαδρομή της δημιουργίας λειτουργιών, πρέπει να περιηγηθούμε ατομικά στη pathμεταβλητή πεδίου χωρίζοντάς την σύμφωνα με τα παρακάτω:

Η παραπάνω createFunctionsμέθοδος μεταβιβάζει το άτομο fieldNameκαι τον κάτοχο της κατηγορίας του στη createFunctionμέθοδο, η οποία θα εντοπίσει τον απαιτούμενο χρήστη javaBeanClass.getDeclaredMethods(). Μόλις εντοπιστεί, χαρτογραφείται σε ένα αντικείμενο Tuple (εγκατάσταση από τη βιβλιοθήκη Vavr), το οποίο περιέχει τον τύπο επιστροφής της μεθόδου λήψης και τη δυναμικά δημιουργημένη λειτουργία στην οποία θα ενεργήσει σαν να ήταν η ίδια η πραγματική μέθοδος λήψης.

Αυτή η χαρτογράφηση tuple γίνεται createTupleWithReturnTypeAndGetterσε συνδυασμό με τη createCallSiteμέθοδο ως εξής:

Στις παραπάνω δύο μεθόδους, χρησιμοποιώ μια σταθερά που ονομάζεται LOOKUP, η οποία είναι απλώς μια αναφορά στο MethodHandles.Lookup. Με αυτό, μπορώ να δημιουργήσω μια άμεση λαβή μεθόδου βάσει της προηγούμενης μεθόδου λήψης. Και τέλος, το δημιουργημένο 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 επαναλήψεις του 1 δευτερολέπτου η καθεμία για να μετρηθεί πραγματικά. Στη συνέχεια, κάθε σενάριο θα επαναληφθεί 3 φορές για να συγκεντρωθούν εύλογα οι μετρήσεις.

Αποτελέσματα

Ας ξεκινήσουμε με τα αποτελέσματα που συγκεντρώθηκαν από το JDK 8u191τρέξιμο:

Το χειρότερο σενάριο που χρησιμοποιεί την invokedynamicπροσέγγιση είναι πολύ πιο γρήγορο από το ταχύτερο σενάριο από τις άλλες δύο βιβλιοθήκες. Αυτή είναι μια τεράστια διαφορά και αν αμφιβάλλετε για τα αποτελέσματα, μπορείτε πάντα να κατεβάσετε τον πηγαίο κώδικα και να παίξετε όπως θέλετε.

Τώρα, ας δούμε πώς λειτουργεί το ίδιο σημείο αναφοράς GraalVM EE 1.0.0-rc9

Μπορείτε να δείτε τα πλήρη αποτελέσματα εδώ με τον ωραίο JMH Visualizer.

Παρατηρήσεις

Η τεράστια διαφορά είναι επειδή το JIT-compiler γνωρίζει CallSiteκαι MethodHandleπολύ καλά και ξέρει πώς να τα ενσωματώσει αρκετά καλά σε αντίθεση με την προσέγγιση προβληματισμού. Επίσης, μπορείτε να δείτε πόσο ελπιδοφόρο είναι το GraalVM. Ο μεταγλωττιστής του κάνει μια πραγματικά φοβερή δουλειά που μπορεί να βελτιώσει την απόδοση για την προσέγγιση προβληματισμού.

Εάν είστε περίεργοι και θέλετε να παίξετε περισσότερο, σας ενθαρρύνω να τραβήξετε τον πηγαίο κώδικα από το Github μου. Λάβετε υπόψη ότι δεν σας ενθαρρύνω να κάνετε το δικό σας σπιτικό JavaBeanUtilκαι να το χρησιμοποιήσετε στην παραγωγή. Αντίθετα, ο στόχος μου εδώ είναι να δείξω απλώς το πείραμά μου και τις δυνατότητες που μπορούμε να πάρουμε invokedynamic.