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

Κάντε Java γρήγορα: Βελτιστοποιήστε!

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

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

  • Η βελτιστοποίηση τείνει να κάνει τον κώδικα πιο δύσκολο να κατανοηθεί και να διατηρηθεί

  • Μερικές από τις τεχνικές που παρουσιάζονται εδώ αυξάνουν την ταχύτητα μειώνοντας την επεκτασιμότητα του κώδικα

  • Η βελτιστοποίηση κώδικα για μία πλατφόρμα μπορεί στην πραγματικότητα να το επιδεινώσει σε άλλη πλατφόρμα

  • Πολύς χρόνος μπορεί να αφιερωθεί βελτιστοποιώντας, με μικρό κέρδος στην απόδοση, και μπορεί να οδηγήσει σε ασαφή κώδικα

  • Εάν είστε υπερβολικά παθιασμένοι με τη βελτιστοποίηση κώδικα, οι άνθρωποι θα σας αποκαλούν σπασίκλα πίσω από την πλάτη σας

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

Γιατί λοιπόν να βελτιστοποιήσετε;

Εάν είναι τόσο κακή ιδέα, γιατί να βελτιστοποιήσετε καθόλου; Λοιπόν, σε έναν ιδανικό κόσμο δεν θα το κάνατε. Αλλά η πραγματικότητα είναι ότι μερικές φορές το μεγαλύτερο πρόβλημα με ένα πρόγραμμα είναι ότι απαιτεί πάρα πολλούς πόρους και αυτοί οι πόροι (μνήμη, κύκλοι CPU, εύρος ζώνης δικτύου ή ένας συνδυασμός) μπορεί να είναι περιορισμένοι. Τα τμήματα κώδικα που εμφανίζονται πολλές φορές σε ένα πρόγραμμα είναι πιθανό να είναι ευαίσθητα στο μέγεθος, ενώ ο κώδικας με πολλές επαναλήψεις εκτέλεσης μπορεί να είναι ευαίσθητος στην ταχύτητα.

Κάντε Java γρήγορα!

Ως ερμηνευμένη γλώσσα με συμπαγή bytecode, ταχύτητα ή έλλειψη αυτής, είναι αυτό που εμφανίζεται πιο συχνά ως πρόβλημα στην Java. Θα εξετάσουμε κυρίως πώς να κάνουμε την Java να τρέχει γρηγορότερα και όχι να ταιριάζει σε μικρότερο χώρο - αν και θα επισημάνουμε πού και πώς αυτές οι προσεγγίσεις επηρεάζουν τη μνήμη ή το εύρος ζώνης δικτύου. Η εστίαση θα είναι στη βασική γλώσσα και όχι στα Java APIs.

Παρεμπιπτόντως, ένα πράγμα εμείς συνηθισμένος συζητήστε εδώ είναι η χρήση εγγενών μεθόδων γραμμένων σε C ή συνέλευση. Ενώ η χρήση εγγενών μεθόδων μπορεί να δώσει την απόλυτη ώθηση στην απόδοση, το κάνει με το κόστος της ανεξαρτησίας της πλατφόρμας Java. Είναι δυνατό να γράψετε τόσο μια έκδοση Java μιας μεθόδου όσο και τις εγγενείς εκδόσεις για επιλεγμένες πλατφόρμες. Αυτό οδηγεί σε αυξημένη απόδοση σε ορισμένες πλατφόρμες χωρίς να παραιτηθεί η δυνατότητα εκτέλεσης σε όλες τις πλατφόρμες. Αλλά αυτό είναι το μόνο που πρόκειται να πω σχετικά με το θέμα της αντικατάστασης της Java με κώδικα C. (Ανατρέξτε στην Συμβουλή Java, "Γράψτε εγγενείς μεθόδους" για περισσότερες πληροφορίες σχετικά με αυτό το θέμα.) Η εστίασή μας σε αυτό το άρθρο είναι το πώς να κάνετε γρήγορη την Java.

90/10, 80/20, καλύβα, καλύβα, πεζοπορία!

Κατά κανόνα, το 90 τοις εκατό του χρόνου εξαίρεσης ενός προγράμματος ξοδεύεται εκτελώντας το 10 τοις εκατό του κώδικα. (Μερικοί άνθρωποι χρησιμοποιούν τον κανόνα 80 τοις εκατό / 20 τοις εκατό, αλλά η εμπειρία μου που γράφω και βελτιστοποιώ τα εμπορικά παιχνίδια σε πολλές γλώσσες τα τελευταία 15 χρόνια έχει δείξει ότι ο τύπος 90 τοις εκατό / 10 τοις εκατό είναι τυπικός για προγράμματα που πεινούν την απόδοση, καθώς λίγες εργασίες τείνουν να εκτελείται με μεγάλη συχνότητα.) Η βελτιστοποίηση του άλλου 90% του προγράμματος (όπου ξοδεύτηκε το 10% του χρόνου εκτέλεσης) δεν έχει αξιοσημείωτη επίδραση στην απόδοση. Εάν μπορούσατε να κάνετε το 90 τοις εκατό του κώδικα να εκτελεστεί δύο φορές πιο γρήγορα, το πρόγραμμα θα ήταν μόνο 5 τοις εκατό πιο γρήγορα. Έτσι, το πρώτο καθήκον στη βελτιστοποίηση του κώδικα είναι να προσδιορίσουμε το 10 τοις εκατό (συχνά είναι μικρότερο από αυτό) του προγράμματος που καταναλώνει το μεγαλύτερο μέρος του χρόνου εκτέλεσης. Αυτό δεν είναι πάντα εκεί που περιμένετε.

Γενικές τεχνικές βελτιστοποίησης

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

Μείωση δύναμης

Η μείωση της αντοχής συμβαίνει όταν μια λειτουργία αντικαθίσταται από μια ισοδύναμη λειτουργία που εκτελείται γρηγορότερα. Το πιο συνηθισμένο παράδειγμα μείωσης ισχύος είναι η χρήση του τελεστή μετατόπισης για πολλαπλασιασμό και διαίρεση ακέραιων με ισχύ 2. Για παράδειγμα, x >> 2 μπορεί να χρησιμοποιηθεί στη θέση του x / 4, και x << 1 αντικαθιστά x * 2.

Συχνή εξάλειψη της έκφρασης

Η κοινή εξάλειψη της έκφρασης αφαιρεί περιττούς υπολογισμούς. Αντί να γράφετε

διπλό x = d * (lim / max) * sx; διπλό y = d * (lim / max) * sy;

Η κοινή έκφραση υπολογίζεται μία φορά και χρησιμοποιείται και για τους δύο υπολογισμούς:

διπλό βάθος = d * (lim / max); διπλό x = βάθος * sx; διπλό y = βάθος * sy;

Κίνηση κώδικα

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

για (int i = 0; i <x.length; i ++) x [i] * = Math.PI * Math.cos (y); 

γίνεται

double picosy = Math.PI * Math.cos (y);για (int i = 0; i <x.length; i ++) x [i] * = picosy; 

Ξετυλίγοντας βρόχους

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

double picosy = Math.PI * Math.cos (y);για (int i = 0; i <x.length; i + = 2) { x [i] * = picosy; x [i + 1] * = picosy; } 

Στην πράξη, το ξετύλιγμα βρόχων όπως αυτό - στον οποίο η τιμή του δείκτη βρόχου χρησιμοποιείται εντός του βρόχου και πρέπει να αυξηθεί ξεχωριστά - δεν αποδίδει σημαντική αύξηση ταχύτητας στην ερμηνευμένη Java επειδή οι κωδικοί bytecodes δεν διαθέτουν οδηγίες για τον αποτελεσματικό συνδυασμό του "+1"στο ευρετήριο του πίνακα.

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

Θέτοντας σε λειτουργία τον μεταγλωττιστή

Οι σύγχρονοι μεταγλωττιστές C και Fortran παράγουν εξαιρετικά βελτιστοποιημένο κώδικα. Οι μεταγλωττιστές C ++ παράγουν γενικά λιγότερο αποδοτικό κώδικα, αλλά εξακολουθούν να είναι καλά στο δρόμο για την παραγωγή βέλτιστου κώδικα. Όλοι αυτοί οι μεταγλωττιστές έχουν περάσει από πολλές γενιές υπό την επήρεια ισχυρού ανταγωνισμού στην αγορά και έχουν γίνει εξαιρετικά ακονισμένα εργαλεία για τη συμπίεση κάθε τελευταίας πτώσης απόδοσης από τον συνηθισμένο κώδικα. Χρησιμοποιούν σχεδόν σίγουρα όλες τις γενικές τεχνικές βελτιστοποίησης που παρουσιάζονται παραπάνω. Αλλά υπάρχουν ακόμα πολλά κόλπα για να κάνουν τους μεταγλωττιστές να παράγουν αποδοτικό κώδικα.

javac, JIT και εγγενείς μεταγλωττιστές κώδικα

Το επίπεδο βελτιστοποίησης που javac αποδίδει κατά την κατάρτιση κώδικα σε αυτό το σημείο είναι ελάχιστη. Από προεπιλογή να κάνετε τα εξής:

  • Σταθερή αναδίπλωση - ο μεταγλωττιστής επιλύει τυχόν σταθερές εκφράσεις έτσι i = (10 * 10) μεταγλωττίζεται σε i = 100.

  • Αναδίπλωση κλαδιών (τις περισσότερες φορές) - περιττή παω σε αποφεύγονται κωδικοί bytec.

  • Περιορισμένη απαλοιφή νεκρού κώδικα - δεν παράγεται κωδικός για δηλώσεις όπως αν (false) i = 1.

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

Στη συνέχεια, υπάρχουν μεταγλωττιστές just-in-time (JIT) που μετατρέπουν τους bytecodes Java σε εγγενή κώδικα κατά το χρόνο εκτέλεσης. Πολλά είναι ήδη διαθέσιμα και ενώ μπορούν να αυξήσουν δραματικά την ταχύτητα εκτέλεσης του προγράμματος σας, το επίπεδο βελτιστοποίησης που μπορούν να εκτελέσουν περιορίζεται επειδή η βελτιστοποίηση πραγματοποιείται κατά το χρόνο εκτέλεσης. Ένας μεταγλωττιστής JIT ασχολείται περισσότερο με τη δημιουργία του κώδικα γρήγορα παρά με τη δημιουργία του ταχύτερου κώδικα.

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

javac προσφέρει μια επιλογή απόδοσης που μπορείτε να ενεργοποιήσετε: επίκληση του επιλογή να αναγκάσει τον μεταγλωττιστή να ενσωματώσει ορισμένες κλήσεις μεθόδου:

javac -O MyClass

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

Δυστυχώς, οι εκδόσεις 1.0 του μεταγλωττιστή javac έχουν ένα σφάλμα που θα δημιουργήσει κώδικα που δεν μπορεί να περάσει τον επαληθευτή bytecode όταν χρησιμοποιείται η επιλογή. Αυτό έχει επιδιορθωθεί στο JDK 1.1. (Ο επαληθευτής bytecode ελέγχει τον κώδικα προτού επιτραπεί να εκτελεστεί για να βεβαιωθείτε ότι δεν παραβιάζει κανέναν κανόνα Java.) Θα ενσωματώσει μεθόδους που αναφέρουν τα μέλη της τάξης μη προσβάσιμα στην κλάση κλήσεων. Για παράδειγμα, εάν οι ακόλουθες τάξεις συγκεντρώνονται χρησιμοποιώντας το επιλογή

κλάση Α {ιδιωτικό στατικό int x = 10; δημόσιο στατικό κενό getX () {return x; }} κλάση B {int y = A.getX (); } 

η κλήση προς A.getX () στην τάξη Β θα είναι ευθυγραμμισμένη στην τάξη Β σαν να γράφτηκε ο Β ως:

κλάση B {int y = A.x; } 

Ωστόσο, αυτό θα προκαλέσει τη δημιουργία κωδικών bytec για πρόσβαση στην ιδιωτική μεταβλητή A.x που θα δημιουργηθεί στον κώδικα του Β. Αυτός ο κωδικός θα εκτελέσει πρόστιμο, αλλά επειδή παραβιάζει τους περιορισμούς πρόσβασης της Java, θα επισημανθεί από τον επαληθευτή με ένα Παράνομο σφάλμα την πρώτη φορά που εκτελείται ο κωδικός.

Αυτό το σφάλμα δεν κάνει το επιλογή άχρηστη, αλλά πρέπει να είστε προσεκτικοί σχετικά με τον τρόπο που το χρησιμοποιείτε. Εάν καλείται σε μία τάξη, μπορεί να ενσωματώσει ορισμένες κλήσεις μεθόδου μέσα στην τάξη χωρίς κίνδυνο. Πολλές τάξεις μπορούν να ενσωματωθούν μαζί, εφόσον δεν υπάρχουν πιθανοί περιορισμοί πρόσβασης. Και ορισμένος κώδικας (όπως εφαρμογές) δεν υπόκειται στον επαληθευτή bytecode. Μπορείτε να αγνοήσετε το σφάλμα εάν γνωρίζετε ότι ο κώδικάς σας θα εκτελεστεί μόνο χωρίς να υποβληθεί στον επαληθευτή. Για περισσότερες πληροφορίες, ανατρέξτε στις Συχνές ερωτήσεις javac-O.

Προφίλ

Ευτυχώς, το JDK έρχεται με ένα ενσωματωμένο προφίλ για να σας βοηθήσει να εντοπίσετε πού ξοδεύεται ο χρόνος σε ένα πρόγραμμα. Θα παρακολουθεί τον χρόνο που αφιερώνεται σε κάθε ρουτίνα και θα γράφει τις πληροφορίες στο αρχείο java.prof. Για να εκτελέσετε το προφίλ, χρησιμοποιήστε το -προφ επιλογή κατά την επίκληση του διερμηνέα Java:

java -prof myClass

Ή για χρήση με μια μικροεφαρμογή:

java -prof sun.applet.AppletViewer myApplet.html

Υπάρχουν μερικές προειδοποιήσεις για τη χρήση του προφίλ. Η έξοδος προφίλ δεν είναι ιδιαίτερα εύκολο να αποκρυπτογραφηθεί. Επίσης, στο JDK 1.0.2 περικόπτει τα ονόματα των μεθόδων σε 30 χαρακτήρες, οπότε ενδέχεται να μην είναι δυνατή η διάκριση ορισμένων μεθόδων. Δυστυχώς, με το Mac δεν υπάρχει τρόπος να επικαλεστεί το προφίλ, έτσι οι χρήστες Mac δεν έχουν τύχη. Πάνω από όλα αυτά, η σελίδα εγγράφων Java της Sun (βλ. Πόροι) δεν περιλαμβάνει πλέον την τεκμηρίωση για το -προφ επιλογή). Ωστόσο, εάν η πλατφόρμα σας υποστηρίζει το -προφ Επιλογή, είτε το HyperProf του Vladimir Bulatov είτε το ProfileViewer του Greg White μπορούν να χρησιμοποιηθούν για την ερμηνεία των αποτελεσμάτων (βλ. Πόροι).

Είναι επίσης δυνατό να "προφίλ" κώδικα εισάγοντας ρητό χρονισμό στον κώδικα:

μακρά εκκίνηση = System.currentTimeMillis (); // κάνει τη λειτουργία να χρονομετρηθεί εδώ πολύ καιρό = System.currentTimeMillis () - start;

System.currentTimeMillis () επιστρέφει χρόνο σε 1 / 1000ths του δευτερολέπτου. Ωστόσο, ορισμένα συστήματα, όπως ένας υπολογιστής με Windows, έχουν χρονοδιακόπτη συστήματος με λιγότερη (πολύ λιγότερη) ανάλυση από ό, τι το 1 / 1000ο του δευτερολέπτου. Ακόμα και το 1 / 1000ο του δευτερολέπτου δεν είναι αρκετό για να διαρκέσει με ακρίβεια πολλές εργασίες. Σε αυτές τις περιπτώσεις, ή σε συστήματα με χρονοδιακόπτες χαμηλής ανάλυσης, μπορεί να χρειαστεί ο χρόνος για τον χρόνο που απαιτείται για την επανάληψη της λειτουργίας ν φορές και στη συνέχεια διαιρέστε το συνολικό χρόνο με ν για να πάρει τον πραγματικό χρόνο. Ακόμα και όταν είναι διαθέσιμο το προφίλ, αυτή η τεχνική μπορεί να είναι χρήσιμη για το χρονοδιάγραμμα μιας συγκεκριμένης εργασίας ή λειτουργίας.

Ακολουθούν μερικές τελικές σημειώσεις σχετικά με το προφίλ:

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

  • Προσπαθήστε να κάνετε κάθε δοκιμή χρονισμού υπό ίδιες συνθήκες

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

Η μικροεφαρμογή συγκριτικής αξιολόγησης

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