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

Βελτιστοποίηση απόδοσης JVM, Μέρος 2: Μεταγλωττιστές

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

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

Αυτό το δεύτερο άρθρο στο Βελτιστοποίηση απόδοσης JVM Η σειρά επισημαίνει και εξηγεί τις διαφορές μεταξύ διαφόρων μεταγλωττιστών εικονικής μηχανής Java. Θα συζητήσω επίσης μερικές κοινές βελτιστοποιήσεις που χρησιμοποιούνται από τους μεταγλωττιστές Just-In-Time (JIT) για Java. (Δείτε "Βελτιστοποίηση απόδοσης JVM, Μέρος 1" για μια επισκόπηση JVM και εισαγωγή στη σειρά.)

Τι είναι ο μεταγλωττιστής;

Απλά μιλώντας a μεταγλωττιστής παίρνει μια γλώσσα προγραμματισμού ως είσοδο και παράγει μια εκτελέσιμη γλώσσα ως έξοδο. Ένας κοινώς γνωστός μεταγλωττιστής είναι javac, το οποίο περιλαμβάνεται σε όλα τα τυπικά κιτ ανάπτυξης Java (JDK). javac παίρνει τον κώδικα Java ως είσοδο και τον μεταφράζει σε bytecode - την εκτελέσιμη γλώσσα για ένα JVM. Ο κωδικός bytec αποθηκεύεται σε αρχεία .class που φορτώνονται στον χρόνο εκτέλεσης Java κατά την εκκίνηση της διαδικασίας Java.

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

Bytecode και το JVM

Αν θέλετε να μάθετε περισσότερα για το bytecode και το JVM, ανατρέξτε στην ενότητα "Βασικά στοιχεία Bytecode" (Bill Venners, JavaWorld).

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

Στατική εναντίον δυναμικής συλλογής

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

Σε μια στατική συλλογή, ο ακόλουθος κώδικας Java

στατικό int add7 (int x) {return x + 7; }

θα οδηγούσε σε κάτι παρόμοιο με αυτόν τον bytecode:

iload0 bipush 7 iadd ireturn

Ένας δυναμικός μεταγλωττιστής μεταφράζει από τη μία γλώσσα στην άλλη δυναμικά, πράγμα που σημαίνει ότι συμβαίνει καθώς ο κώδικας εκτελείται - κατά τη διάρκεια του χρόνου εκτέλεσης! Η δυναμική συλλογή και βελτιστοποίηση δίνουν στους χρόνους εκτέλεσης το πλεονέκτημα ότι μπορούν να προσαρμοστούν σε αλλαγές στο φορτίο της εφαρμογής. Οι δυναμικοί μεταγλωττιστές ταιριάζουν πολύ στους χρόνους εκτέλεσης Java, οι οποίοι συνήθως εκτελούνται σε απρόβλεπτα και συνεχώς μεταβαλλόμενα περιβάλλοντα. Τα περισσότερα JVM χρησιμοποιούν έναν δυναμικό μεταγλωττιστή όπως έναν μεταγλωττιστή Just-In-Time (JIT). Το πλεονέκτημα είναι ότι οι δυναμικοί μεταγλωττιστές και η βελτιστοποίηση κώδικα χρειάζονται μερικές φορές επιπλέον δομές δεδομένων, νήμα και πόρους CPU. Όσο πιο προχωρημένη είναι η βελτιστοποίηση ή η ανάλυση περιβάλλοντος bytecode, τόσο περισσότεροι πόροι καταναλώνονται από τη συλλογή. Στα περισσότερα περιβάλλοντα, τα γενικά έξοδα εξακολουθούν να είναι πολύ μικρά σε σύγκριση με το σημαντικό κέρδος απόδοσης του κώδικα εξόδου.

Ποικιλίες JVM και ανεξαρτησία πλατφόρμας Java

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

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

Από Java bytecode έως εκτέλεση

Μόλις ο κώδικας Java σας μεταγλωττιστεί σε bytecode, τα επόμενα βήματα είναι να μεταφράσετε τις οδηγίες bytecode σε κώδικα μηχανήματος. Αυτό μπορεί να γίνει είτε από διερμηνέα είτε από μεταγλωττιστή.

Ερμηνεία

Η απλούστερη μορφή συλλογής bytecode ονομάζεται ερμηνεία. Ενα διερμηνέας απλώς αναζητά τις οδηγίες υλικού για κάθε εντολή bytecode και την αποστέλλει για εκτέλεση από την CPU.

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

Συλλογή

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

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

Βελτιστοποίηση

Μαζί με τη δυναμική συλλογή έρχεται η ευκαιρία να εισαχθούν μετρητές επιδόσεων. Ο μεταγλωττιστής μπορεί, για παράδειγμα, να εισαγάγει ένα μετρητής επιδόσεων για μέτρηση κάθε φορά που κλήθηκε ένα μπλοκ bytecode (π.χ., που αντιστοιχεί σε μια συγκεκριμένη μέθοδο). Οι μεταγλωττιστές χρησιμοποιούν δεδομένα σχετικά με το πόσο "καυτό" είναι ένας δεδομένος bytecode για να προσδιορίσουν πού θα βελτιστοποιήσουν καλύτερα οι εφαρμογές βελτιστοποίησης κώδικα. Τα δεδομένα προφίλ χρόνου εκτέλεσης επιτρέπουν στον μεταγλωττιστή να λαμβάνει ένα πλούσιο σύνολο αποφάσεων βελτιστοποίησης κώδικα εν κινήσει, βελτιώνοντας περαιτέρω την απόδοση εκτέλεσης κώδικα. Καθώς καθίστανται διαθέσιμα περισσότερα βελτιωμένα δεδομένα προφίλ κώδικα, μπορεί να χρησιμοποιηθεί για τη λήψη πρόσθετων και καλύτερων αποφάσεων βελτιστοποίησης, όπως: πώς να βελτιώσετε τις οδηγίες ακολουθίας στη γλώσσα μεταγλωττισμένων προς, εάν θα αντικαταστήσετε ένα σύνολο οδηγιών με πιο αποτελεσματικά σύνολα ή ακόμα και εάν θα εξαλειφθούν οι περιττές λειτουργίες.

Παράδειγμα

Εξετάστε τον κώδικα Java:

στατικό int add7 (int x) {return x + 7; }

Αυτό θα μπορούσε να συνταχθεί στατικά από javac στο bytecode:

iload0 bipush 7 iadd ireturn

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

lea rax, [rdx + 7] ret

Διαφορετικοί μεταγλωττιστές για διαφορετικές εφαρμογές

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

Μεταγλωττιστές από πλευράς πελάτη

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

Μεταγλωττιστές από διακομιστή

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

Συμβουλή: Προθέρμανση του μεταγλωττιστή από την πλευρά του διακομιστή

Για αναπτύξεις από την πλευρά του διακομιστή μπορεί να χρειαστεί λίγος χρόνος προτού ο μεταγλωττιστής βελτιστοποιήσει τα αρχικά "καυτά" μέρη του κώδικα, οπότε οι αναπτύξεις από την πλευρά του διακομιστή συχνά απαιτούν φάση "προθέρμανσης". Πριν πραγματοποιήσετε οποιαδήποτε μέτρηση απόδοσης σε ανάπτυξη από διακομιστή, βεβαιωθείτε ότι η εφαρμογή σας έχει φτάσει σε σταθερή κατάσταση! Αν αφήσετε τον μεταγλωττιστή αρκετό χρόνο για σωστή μεταγλώττιση, θα λειτουργήσει προς όφελός σας! (Ανατρέξτε στο άρθρο JavaWorld "Παρακολουθήστε το HotSpot μεταγλωττιστή σας" για περισσότερα σχετικά με την προθέρμανση του μεταγλωττιστή σας και τους μηχανισμούς δημιουργίας προφίλ.)

Ένας μεταγλωττιστής διακομιστών αντιπροσωπεύει περισσότερα προφίλ δεδομένων από ό, τι κάνει ένας μεταγλωττιστής από πλευράς πελάτη και επιτρέπει πιο σύνθετη ανάλυση κλάδου, πράγμα που σημαίνει ότι θα εξετάσει ποια διαδρομή βελτιστοποίησης θα ήταν πιο επωφελής. Έχοντας διαθέσιμα περισσότερα δεδομένα προφίλ, αποδίδουμε καλύτερα αποτελέσματα εφαρμογής. Φυσικά, η πιο εκτεταμένη δημιουργία προφίλ και ανάλυση απαιτεί την εξόφληση περισσότερων πόρων στον μεταγλωττιστή. Ένα JVM με ενεργοποιημένο το C2 θα χρησιμοποιεί περισσότερα νήματα και περισσότερους κύκλους CPU, θα απαιτεί μεγαλύτερο cache κώδικα και ούτω καθεξής.

Βαθμιδωτή συλλογή

Βαθμιδωτή συλλογή συνδυάζει τη συλλογή πελατών και διακομιστών. Η Azul για πρώτη φορά έκανε μια κλιμακωτή συλλογή στο Zing JVM. Πιο πρόσφατα (από το Java SE 7) έχει υιοθετηθεί από την Oracle Java Hotspot JVM. Η κλιμακωτή συλλογή εκμεταλλεύεται τα πλεονεκτήματα τόσο του προγράμματος-πελάτη όσο και του διακομιστή στο JVM. Ο μεταγλωττιστής πελατών είναι πιο ενεργός κατά την εκκίνηση της εφαρμογής και χειρίζεται βελτιστοποιήσεις που ενεργοποιούνται από χαμηλότερα όρια απόδοσης. Ο μεταγλωττιστής από την πλευρά του πελάτη εισάγει επίσης μετρητές επιδόσεων και προετοιμάζει σύνολα οδηγιών για πιο προηγμένες βελτιστοποιήσεις, οι οποίες θα αντιμετωπιστούν αργότερα από τον μεταγλωττιστή από την πλευρά του διακομιστή. Η κλιμακωτή συλλογή είναι ένας πολύ αποδοτικός πόρος τρόπος δημιουργίας προφίλ, επειδή ο μεταγλωττιστής είναι σε θέση να συλλέγει δεδομένα κατά τη διάρκεια της δραστηριότητας μεταγλωττιστή χαμηλού αντίκτυπου, η οποία μπορεί να χρησιμοποιηθεί αργότερα για πιο προηγμένες βελτιστοποιήσεις. Αυτή η προσέγγιση αποδίδει επίσης περισσότερες πληροφορίες από ό, τι θα λάβετε από τη χρήση ερμηνευμένων μετρητών προφίλ κώδικα.

Το σχήμα γραφήματος στο Σχήμα 1 απεικονίζει τις διαφορές απόδοσης μεταξύ της καθαρής ερμηνείας, της πελατειακής πλευράς, της πλευράς διακομιστή και της κλιμακωτής συλλογής. Ο άξονας X δείχνει τον χρόνο εκτέλεσης (μονάδα χρόνου) και την απόδοση του άξονα Υ (μονάδα λειτουργίας / ώρα).

Σχήμα 1. Διαφορές απόδοσης μεταξύ μεταγλωττιστών (κάντε κλικ για μεγέθυνση)

Σε σύγκριση με τον καθαρά ερμηνευμένο κώδικα, η χρήση ενός μεταγλωττιστή από την πλευρά του πελάτη οδηγεί σε περίπου 5 έως 10 φορές καλύτερη απόδοση εκτέλεσης (σε ops / s), βελτιώνοντας έτσι την απόδοση της εφαρμογής. Η διακύμανση του κέρδους εξαρτάται φυσικά από το πόσο αποτελεσματικός είναι ο μεταγλωττιστής, ποιες βελτιστοποιήσεις ενεργοποιούνται ή εφαρμόζονται και (σε ​​μικρότερο βαθμό) πόσο καλά σχεδιασμένη είναι η εφαρμογή σε σχέση με την πλατφόρμα στόχου εκτέλεσης. Ωστόσο, το τελευταίο είναι κάτι που ένας προγραμματιστής Java δεν πρέπει ποτέ να ανησυχεί.

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