Προγραμματισμός

Προγραμματισμός απόδοσης Java, Μέρος 2: Το κόστος της μετάδοσης

Για αυτό το δεύτερο άρθρο της σειράς μας σχετικά με την απόδοση της Java, η εστίαση μετατοπίζεται στο cast - τι είναι, τι κοστίζει και πώς μπορούμε (μερικές φορές) να το αποφύγουμε. Αυτό το μήνα, ξεκινάμε με μια γρήγορη ανασκόπηση των βασικών τάξεων, των αντικειμένων και των αναφορών και, στη συνέχεια, παρακολουθούμε μερικές από τις σκληρές φιγούρες απόδοσης (σε μια πλαϊνή μπάρα, ώστε να μην προσβάλλουμε το squeamish!) Και οδηγίες σχετικά με τύποι λειτουργιών που είναι πιο πιθανό να προκαλέσουν δυσπεψία στην εικονική μηχανή Java (JVM). Τέλος, ολοκληρώνουμε με μια εις βάθος ματιά στο πώς μπορούμε να αποφύγουμε τα κοινά εφέ δομής τάξης που μπορούν να προκαλέσουν μετάδοση.

Προγραμματισμός απόδοσης Java: Διαβάστε ολόκληρη τη σειρά!

  • Μέρος 1. Μάθετε πώς να μειώσετε τα γενικά προγράμματα και να βελτιώσετε την απόδοση ελέγχοντας τη δημιουργία αντικειμένων και τη συλλογή απορριμμάτων
  • Μέρος 2. Μειώστε τα γενικά σφάλματα και τα σφάλματα εκτέλεσης μέσω κωδικού ασφαλούς τύπου
  • Μέρος 3. Δείτε πώς οι εναλλακτικές συλλογές μετράνε την απόδοση και μάθετε πώς να αξιοποιήσετε στο έπακρο κάθε τύπο

Τύποι αντικειμένων και αναφοράς σε Java

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

Κάθε ορισμός κλάσης σε ένα πρόγραμμα Java ορίζει έναν νέο τύπο αντικειμένου. Αυτό περιλαμβάνει όλες τις τάξεις από τις βιβλιοθήκες Java, οπότε οποιοδήποτε δεδομένο πρόγραμμα μπορεί να χρησιμοποιεί εκατοντάδες ή και χιλιάδες διαφορετικούς τύπους αντικειμένων. Μερικοί από αυτούς τους τύπους καθορίζονται από τον ορισμό της γλώσσας Java ως ορισμένων ειδικών χρήσεων ή χειρισμού (όπως η χρήση του java.lang.StringBuffer Για java.lang.String πράξεις συνένωσης). Εκτός από αυτές τις λίγες εξαιρέσεις, ωστόσο, όλοι οι τύποι αντιμετωπίζονται βασικά ο ίδιος από τον μεταγλωττιστή Java και το JVM που χρησιμοποιήθηκε για την εκτέλεση του προγράμματος.

Εάν ένας ορισμός κλάσης δεν προσδιορίζει (μέσω του εκτείνεται ρήτρα στην κεφαλίδα ορισμού κλάσης) μια άλλη κλάση ως γονέα ή superclass, επεκτείνει σιωπηρά το java.lang.Object τάξη. Αυτό σημαίνει ότι κάθε τάξη επεκτείνεται τελικά java.lang.Object, είτε άμεσα είτε μέσω μιας ακολουθίας ενός ή περισσότερων επιπέδων γονικών τάξεων.

Τα ίδια τα αντικείμενα είναι πάντα περιπτώσεις τάξεων και αντικειμένων τύπος είναι η τάξη της οποίας είναι ένα παράδειγμα. Στην Java, όμως, δεν ασχολούμαστε ποτέ άμεσα με αντικείμενα. δουλεύουμε με αναφορές σε αντικείμενα. Για παράδειγμα, η γραμμή:

 java.awt.Component myComponent; 

δεν δημιουργεί ένα java.awt.Component αντικείμενο; δημιουργεί μια μεταβλητή αναφοράς τύπου java.lang.Component. Παρόλο που οι αναφορές έχουν τύπους όπως τα αντικείμενα, δεν υπάρχει ακριβής αντιστοίχιση μεταξύ τύπων αναφοράς και αντικειμένων - μπορεί να είναι μια τιμή αναφοράς μηδενικό, ένα αντικείμενο του ίδιου τύπου με την αναφορά, ή ένα αντικείμενο οποιασδήποτε υποκατηγορίας (δηλαδή, κλάση που προέρχεται από) τον τύπο της αναφοράς. Σε αυτή τη συγκεκριμένη περίπτωση, java.awt.Component είναι μια αφηρημένη τάξη, οπότε γνωρίζουμε ότι δεν μπορεί ποτέ να υπάρχει αντικείμενο του ίδιου τύπου με την αναφορά μας, αλλά σίγουρα μπορεί να υπάρχουν αντικείμενα υποκατηγοριών αυτού του τύπου αναφοράς.

Πολυμορφισμός και χύτευση

Ο τύπος αναφοράς καθορίζει πώς το αντικείμενο αναφοράς - δηλαδή, το αντικείμενο που είναι η τιμή της αναφοράς - μπορεί να χρησιμοποιηθεί. Για παράδειγμα, στο παραπάνω παράδειγμα, χρησιμοποιήστε κώδικα myComponent θα μπορούσε να επικαλεστεί οποιαδήποτε από τις μεθόδους που ορίζονται από την τάξη java.awt.Component, ή οποιοδήποτε από τα σούπερ γυαλιά του, στο αντικείμενο αναφοράς.

Ωστόσο, η μέθοδος που πραγματικά εκτελείται από μια κλήση καθορίζεται όχι από τον τύπο της ίδιας της αναφοράς, αλλά από τον τύπο του αντικειμένου αναφοράς. Αυτή είναι η βασική αρχή του πολυμορφισμός - οι υποκατηγορίες μπορούν να παρακάμψουν μεθόδους που ορίζονται στη μητρική τάξη προκειμένου να εφαρμόσουν διαφορετική συμπεριφορά. Στην περίπτωση του παραδείγματος μεταβλητής μας, εάν το αντικείμενο αναφοράς ήταν στην πραγματικότητα μια παρουσία του java.awt. Κουμπί, η αλλαγή κατάστασης που προκύπτει από ένα setLabel ("Σπρώξτε με") η κλήση θα ήταν διαφορετική από αυτήν που προκύπτει εάν το αντικείμενο αναφοράς ήταν μια παρουσία του java.awt. Ετικέτα.

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

Χύσιμο χρησιμοποιείται για τη μετατροπή μεταξύ τύπων - συγκεκριμένα μεταξύ τύπων αναφοράς, για τον τύπο λειτουργίας χύτευσης για τον οποίο μας ενδιαφέρει εδώ. Αναβαθμισμένες λειτουργίες (επίσης λέγεται διεύρυνση των μετατροπών στο Java Language Specification) μετατρέψτε μια αναφορά υποκατηγορίας σε μια αναφορά κλάσης προγόνων. Αυτή η λειτουργία χύτευσης είναι συνήθως αυτόματη, καθώς είναι πάντα ασφαλής και μπορεί να εφαρμοστεί απευθείας από τον μεταγλωττιστή.

Λειτουργίες Downcast (επίσης λέγεται μείωση των μετατροπών στο Java Language Specification) μετατρέψτε μια αναφορά κλάσης προγόνων σε μια αναφορά υποκατηγορίας. Αυτή η λειτουργία μετάδοσης δημιουργεί γενικά εκτέλεση, καθώς η Java απαιτεί να ελέγχεται το cast κατά το χρόνο εκτέλεσης για να βεβαιωθεί ότι είναι έγκυρο. Εάν το αντικείμενο αναφοράς δεν είναι παρουσία ούτε του τύπου στόχου για το cast ή μιας υποκατηγορίας αυτού του τύπου, το απόπειρα cast δεν επιτρέπεται και πρέπει να ρίξει ένα java.lang.ClassCastException.

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

Προσέχοντας τους ανέμους

Η μετάδοση επιτρέπει τη χρήση γενικού προγραμματισμού στην Java, όπου ο κώδικας γράφεται για να λειτουργεί με όλα τα αντικείμενα των τάξεων που προέρχονται από κάποια βασική τάξη (συχνά java.lang.Object, για τάξεις χρησιμότητας). Ωστόσο, η χρήση της χύτευσης προκαλεί ένα μοναδικό σύνολο προβλημάτων. Στην επόμενη ενότητα θα δούμε τον αντίκτυπο στην απόδοση, αλλά ας εξετάσουμε πρώτα την επίδραση στον ίδιο τον κώδικα. Εδώ είναι ένα δείγμα που χρησιμοποιεί το γενικό java.lang.Vector τάξη συλλογής:

 ιδιωτικό Vector someNumbers? ... public void doSomething () {... int n = ... Integer number = (Integer) someNumbers.elementAt (n); ...} 

Αυτός ο κώδικας παρουσιάζει πιθανά προβλήματα όσον αφορά τη σαφήνεια και τη συντηρησιμότητα. Εάν κάποιος άλλος από τον αρχικό προγραμματιστή επρόκειτο να τροποποιήσει τον κώδικα σε κάποιο σημείο, μπορεί λογικά να πιστεύει ότι θα μπορούσε να προσθέσει ένα java.lang. Διπλό στο μερικοί αριθμοί συλλογές, δεδομένου ότι αυτή είναι μια υποκατηγορία του java.lang.Number. Όλα θα συνέχιζαν καλά αν δοκίμασε αυτό, αλλά σε κάποιο απροσδιόριστο σημείο εκτέλεσης πιθανότατα θα είχε java.lang.ClassCastException ρίχτηκε όταν η απόπειρα ρίχτηκε σε java.lang.Integer εκτελέστηκε για την προστιθέμενη αξία του.

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

Δεν αποτελεί έκπληξη, η υποστήριξη μιας τεχνικής που θα επέτρεπε στον μεταγλωττιστή να εντοπίσει αυτόν τον τύπο σφάλματος χρήσης είναι μια από τις πιο απαιτούμενες βελτιώσεις στην Java. Υπάρχει ένα έργο σε εξέλιξη στη Διαδικασία κοινότητας Java που διερευνά την προσθήκη μόνο αυτής της υποστήριξης: αριθμός έργου JSR-000014, Προσθήκη γενικών τύπων στη γλώσσα προγραμματισμού Java (ανατρέξτε στην ενότητα Πόροι παρακάτω για περισσότερες λεπτομέρειες.) Στη συνέχεια αυτού του άρθρου, τον επόμενο μήνα, θα εξετάσουμε αυτό το έργο με περισσότερες λεπτομέρειες και θα συζητήσουμε τόσο πώς είναι πιθανό να βοηθήσει όσο και πού είναι πιθανό να μας αφήσει να θέλουμε περισσότερα.

Το ζήτημα της απόδοσης

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

Για αυτό το άρθρο, ανέπτυξα μια σειρά δοκιμών για να δω πόσο σημαντικοί είναι αυτοί οι παράγοντες για την απόδοση με τα τρέχοντα JVMs. Τα αποτελέσματα των δοκιμών συνοψίζονται σε δύο πίνακες στην πλαϊνή γραμμή, ο Πίνακας 1 δείχνει τη μέθοδο υπερυψωμένης κλήσης και ο Πίνακας 2 γενικά. Ο πλήρης πηγαίος κώδικας για το πρόγραμμα δοκιμών είναι επίσης διαθέσιμος στο Διαδίκτυο (ανατρέξτε στην ενότητα Πόροι παρακάτω για περισσότερες λεπτομέρειες).

Για να συνοψίσουμε αυτά τα συμπεράσματα για αναγνώστες που δεν θέλουν να περάσουν από τις λεπτομέρειες στους πίνακες, ορισμένοι τύποι κλήσεων μεθόδων και cast είναι ακόμα αρκετά ακριβοί, σε ορισμένες περιπτώσεις διαρκούν σχεδόν όσο μια απλή κατανομή αντικειμένων. Όπου είναι δυνατόν, αυτοί οι τύποι λειτουργιών πρέπει να αποφεύγονται σε κώδικα που πρέπει να βελτιστοποιηθεί για απόδοση.

Συγκεκριμένα, οι κλήσεις προς μεθόδους παράκαμψης (μέθοδοι που παρακάμπτονται σε οποιαδήποτε φορτωμένη κλάση, όχι μόνο στην πραγματική κατηγορία του αντικειμένου) και οι κλήσεις μέσω διεπαφών είναι πολύ πιο δαπανηρές από τις απλές κλήσεις μεθόδου. Ο διακομιστής HotSpot JVM 2.0 beta που χρησιμοποιείται στη δοκιμή θα μετατρέψει ακόμη και πολλές απλές μεθόδους κλήσεις σε ενσωματωμένο κώδικα, αποφεύγοντας τυχόν γενικά έξοδα για τέτοιες λειτουργίες. Ωστόσο, το HotSpot δείχνει τη χειρότερη απόδοση μεταξύ των δοκιμασμένων JVM για παρακάμψεις μεθόδους και κλήσεις μέσω διεπαφών.

Για μετάδοση (downcasting, φυσικά), οι δοκιμασμένοι JVM διατηρούν γενικά την απόδοση σε ένα λογικό επίπεδο. Το HotSpot κάνει μια εξαιρετική δουλειά με αυτό στις περισσότερες δοκιμές αναφοράς και, όπως και με τις μεθόδους κλήσεων, είναι σε πολλές απλές περιπτώσεις ικανές να εξαλείψουν σχεδόν τελείως τα γενικά έξοδα της μετάδοσης. Για πιο περίπλοκες καταστάσεις, όπως τα cast που ακολουθούνται από κλήσεις για παράκαμψη μεθόδων, όλα τα δοκιμασμένα JVM δείχνουν αισθητή υποβάθμιση της απόδοσης.

Η δοκιμασμένη έκδοση του HotSpot έδειξε επίσης εξαιρετικά κακή απόδοση όταν ένα αντικείμενο μεταδόθηκε σε διαφορετικούς τύπους αναφοράς διαδοχικά (αντί να μεταδίδεται πάντα στον ίδιο τύπο στόχου). Αυτή η κατάσταση εμφανίζεται τακτικά σε βιβλιοθήκες όπως το Swing που χρησιμοποιούν μια βαθιά ιεραρχία τάξεων.

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

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

Βασικές κατηγορίες και casting

Υπάρχουν πολλές κοινές χρήσεις μετάδοσης σε προγράμματα Java. Για παράδειγμα, η μετάδοση χρησιμοποιείται συχνά για τον γενικό χειρισμό ορισμένων λειτουργιών σε μια βασική κλάση που μπορεί να επεκταθεί από έναν αριθμό υποκατηγοριών. Ο παρακάτω κώδικας δείχνει μια κάπως επινοημένη απεικόνιση αυτής της χρήσης:

 // απλή βασική κλάση με υποκατηγορίες δημόσια αφηρημένη κλάση BaseWidget {...} δημόσια κλάση SubWidget επεκτείνει το BaseWidget {... public void doSubWidgetSomething () {...}} ... // βασική κλάση με υποκατηγορίες, χρησιμοποιώντας το προηγούμενο σύνολο των τάξεων δημόσια αφηρημένη κλάση BaseGorph {// το Widget που σχετίζεται με αυτό το Gorph ιδιωτικό BaseWidget myWidget; ... // ορίστε το Widget που σχετίζεται με αυτό το Gorph (επιτρέπεται μόνο για υποκατηγορίες) προστατευμένο void setWidget (widget BaseWidget) {myWidget = widget; } // συνδέστε το Widget με αυτό το Gorph public BaseWidget getWidget () {return myWidget; } ... // επιστρέψτε έναν Γκόρφο με κάποια σχέση με αυτόν τον Γκόρφο // αυτός θα είναι πάντα ο ίδιος τύπος με τον οποίο ζητείται, αλλά μπορούμε // να επιστρέψουμε μόνο μια παρουσία της δημόσιας περίληψης της βασικής κατηγορίας BaseGorph otherGorph () {. ..}} // Gorph subclass χρησιμοποιώντας Widget subclass public class Το SubGorph επεκτείνει το BaseGorph {// επιστρέφει ένα Gorph με κάποια σχέση με αυτό το Gorph public BaseGorph otherGorph () {...} ... public void anyM Method () {.. // ρυθμίστε το Widget που χρησιμοποιούμε το SubWidget widget = ... setWidget (widget); ... // χρησιμοποιήστε το Widget ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // χρησιμοποιήστε το άλλο μαςGorph SubGorph other = (SubGorph) otherGorph (); ...}}