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

Μια υπόθεση για τη διατήρηση πρωτόγονων στην Java

Το Primitives είναι μέρος της γλώσσας προγραμματισμού Java από την αρχική του κυκλοφορία το 1996, και ωστόσο παραμένει ένα από τα πιο αμφιλεγόμενα χαρακτηριστικά γλώσσας. Ο John Moore κάνει μια ισχυρή περίπτωση για τη διατήρηση πρωτόγονων στη γλώσσα Java συγκρίνοντας απλά σημεία αναφοράς Java, τόσο με όσο και χωρίς πρωτόγονα. Στη συνέχεια συγκρίνει την απόδοση της Java με εκείνη του Scala, του C ++ και του JavaScript σε έναν συγκεκριμένο τύπο εφαρμογής, όπου τα πρωτόγονα κάνουν μια αξιοσημείωτη διαφορά.

Ερώτηση: Ποιοι είναι οι τρεις πιο σημαντικοί παράγοντες στην αγορά ακινήτων;

Απάντηση: Τοποθεσία, τοποθεσία, τοποθεσία.

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

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

Η συμπερίληψη των πρωτόγονων στην Java ήταν μία από τις πιο αμφιλεγόμενες αποφάσεις σχεδιασμού γλωσσών, όπως αποδεικνύεται από τον αριθμό των άρθρων και των δημοσιεύσεων φόρουμ που σχετίζονται με αυτήν την απόφαση. Ο Simon Ritter σημείωσε στην κεντρική του ομιλία στο JAX London τον Νοέμβριο του 2011 ότι δόθηκε σοβαρή προσοχή στην απομάκρυνση των πρωτόγονων σε μια μελλοντική έκδοση της Java (βλ. Διαφάνεια 41). Σε αυτό το άρθρο θα παρουσιάσω εν συντομία πρωτόγονα και το σύστημα διπλού τύπου Java. Χρησιμοποιώντας δείγματα κώδικα και απλά σημεία αναφοράς, θα αναφέρω το λόγο για τον οποίο απαιτούνται πρωτότυπα Java για συγκεκριμένους τύπους εφαρμογών. Θα συγκρίνω επίσης την απόδοση της Java με εκείνη των Scala, C ++ και JavaScript.

Μέτρηση της απόδοσης του λογισμικού

Η απόδοση του λογισμικού μετριέται συνήθως σε όρους χρόνου και χώρου. Ο χρόνος μπορεί να είναι πραγματικός χρόνος λειτουργίας, όπως 3,7 λεπτά ή σειρά ανάπτυξης βάσει του μεγέθους της εισόδου, όπως Ο(ν2). Παρόμοια μέτρα ισχύουν για την απόδοση του χώρου, η οποία εκφράζεται συχνά ως προς την κύρια χρήση της μνήμης, αλλά μπορεί επίσης να επεκταθεί και στη χρήση δίσκου. Η βελτίωση της απόδοσης συνεπάγεται συνήθως μια ανταλλαγή χρόνου-χώρου στο ότι οι αλλαγές για τη βελτίωση του χρόνου έχουν συχνά επιζήμια επίδραση στο χώρο και αντίστροφα. Η μέτρηση της τάξης της ανάπτυξης εξαρτάται από τον αλγόριθμο και η μετάβαση από τάξεις περιτυλίγματος σε πρωτόγονες δεν θα αλλάξει το αποτέλεσμα. Όμως, όσον αφορά την πραγματική απόδοση χρόνου και χώρου, η χρήση πρωτότυπων αντί για τάξεις περιτυλίγματος προσφέρει βελτιώσεις ταυτόχρονα στο χρόνο και στο χώρο.

Πρωτόγονα έναντι αντικειμένων

Όπως πιθανώς γνωρίζετε ήδη αν διαβάζετε αυτό το άρθρο, η Java διαθέτει ένα σύστημα διπλού τύπου, που συνήθως αναφέρεται ως πρωτόγονοι τύποι και τύποι αντικειμένων, που συντομογραφούνται απλά ως πρωτόγονα και αντικείμενα. Υπάρχουν οκτώ πρωτόγονοι τύποι προκαθορισμένοι στην Java και τα ονόματά τους είναι δεσμευμένες λέξεις-κλειδιά. Τα παραδείγματα που χρησιμοποιούνται συνήθως περιλαμβάνουν int, διπλό, και boolean. Ουσιαστικά όλοι οι άλλοι τύποι στην Java, συμπεριλαμβανομένων όλων των τύπων που καθορίζονται από το χρήστη, είναι τύποι αντικειμένων. (Λέω "ουσιαστικά" επειδή οι τύποι συστοιχιών είναι λίγο υβριδικοί, αλλά μοιάζουν περισσότερο με τύπους αντικειμένων από πρωτόγονους τύπους.) Για κάθε πρωτόγονο τύπο υπάρχει μια αντίστοιχη τάξη περιτυλίγματος που είναι τύπος αντικειμένου. παραδείγματα περιλαμβάνουν Ακέραιος αριθμός Για int, Διπλό Για διπλό, και Boolean Για boolean.

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

 int n1 = 100; Ακέραιος n2 = νέος ακέραιος (100); 

Χρησιμοποιώντας το αυτόματο κιβώτιο, ένα χαρακτηριστικό που προστέθηκε στο JDK 5, θα μπορούσα να συντομεύσω τη δεύτερη δήλωση σε απλά

 Ακέραιος n2 = 100; 

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

Η διαφορά μεταξύ του πρωτόγονου ν1 και το αντικείμενο περιτύλιξης ν2 απεικονίζεται από το διάγραμμα στο σχήμα 1.

John I. Moore, νεώτερος

Η μεταβλητή ν1 κατέχει ακέραια τιμή, αλλά η μεταβλητή ν2 περιέχει μια αναφορά σε ένα αντικείμενο και είναι το αντικείμενο που κρατά την ακέραια τιμή. Επιπλέον, το αντικείμενο στο οποίο αναφέρεται ν2 περιέχει επίσης μια αναφορά στο αντικείμενο κλάσης Διπλό.

Το πρόβλημα με τα πρωτόγονα

Προτού προσπαθήσω να σας πείσω για την ανάγκη για πρωτόγονους τύπους, πρέπει να αναγνωρίσω ότι πολλοί άνθρωποι δεν θα συμφωνήσουν μαζί μου. Ο Sherman Alpert στο "Primitive types θεωρούνται επιβλαβείς" υποστηρίζει ότι τα πρωτόγονα είναι επιβλαβή επειδή αναμιγνύουν "διαδικαστική σημασιολογία σε ένα κατά τα άλλα ομοιόμορφο αντικειμενοστρεφή μοντέλο. Τα πρωτόγονα δεν είναι αντικείμενα πρώτης κατηγορίας, αλλά υπάρχουν σε μια γλώσσα που περιλαμβάνει, κυρίως αντικείμενα τάξης. " Τα πρωτόγονα και τα αντικείμενα (με τη μορφή τάξεων περιτυλίγματος) παρέχουν δύο τρόπους χειρισμού λογικά παρόμοιων τύπων, αλλά έχουν πολύ διαφορετική υποκείμενη σημασιολογία. Για παράδειγμα, πώς πρέπει να συγκρίνονται δύο περιπτώσεις για ισότητα; Για πρωτόγονους τύπους, κάποιος χρησιμοποιεί το == χειριστή, αλλά για αντικείμενα η προτιμώμενη επιλογή είναι να καλέσετε το ισούται με () μέθοδος, η οποία δεν είναι επιλογή για πρωτόγονους. Ομοίως, υπάρχουν διαφορετικά σημασιολογικά κατά την εκχώρηση τιμών ή παραμέτρων παραμέτρων. Ακόμη και οι προεπιλεγμένες τιμές είναι διαφορετικές. π.χ., 0 Για int εναντίον μηδενικό Για Ακέραιος αριθμός.

Για περισσότερες πληροφορίες σχετικά με αυτό το ζήτημα, ανατρέξτε στην ανάρτηση του Eric Bruno στο blog, "Μια σύγχρονη πρωτόγονη συζήτηση", η οποία συνοψίζει ορισμένα από τα πλεονεκτήματα και τα μειονεκτήματα των πρωτόγονων. Ορισμένες συζητήσεις για το Stack Overflow επικεντρώνονται επίσης σε πρωτόγονες, όπως "Γιατί οι άνθρωποι εξακολουθούν να χρησιμοποιούν πρωτόγονους τύπους στην Java;" και "Υπάρχει λόγος να χρησιμοποιείτε πάντα Αντικείμενα αντί πρωτόγονων;" Οι προγραμματιστές Stack Exchange φιλοξενούν μια παρόμοια συζήτηση με τίτλο "Πότε να χρησιμοποιώ πρωτόγονη εναντίον τάξης στην Java;".

Αξιοποίηση μνήμης

ΕΝΑ διπλό Στην Java καταλαμβάνει πάντα 64 bit στη μνήμη, αλλά το μέγεθος μιας αναφοράς εξαρτάται από την εικονική μηχανή Java (JVM). Ο υπολογιστής μου εκτελεί την έκδοση 64-bit των Windows 7 και ένα JVM 64-bit, και επομένως μια αναφορά στον υπολογιστή μου καταλαμβάνει 64 bit. Με βάση το διάγραμμα στο Σχήμα 1 θα περίμενα ένα διπλό όπως ν1 να καταλάβω 8 bytes (64 bits), και θα περίμενα ένα Διπλό όπως ν2 να καταλάβει 24 bytes - 8 για την αναφορά στο αντικείμενο, 8 για το διπλό τιμή αποθηκευμένη στο αντικείμενο και 8 για την αναφορά στο αντικείμενο κλάσης για Διπλό. Επιπλέον, η Java χρησιμοποιεί επιπλέον μνήμη για να υποστηρίζει τη συλλογή απορριμμάτων για τύπους αντικειμένων αλλά όχι για πρωτόγονους τύπους. Ας το ελέγξουμε.

Χρησιμοποιώντας μια προσέγγιση παρόμοια με εκείνη του Glen McCluskey σε "Java primitive types vs. wrappers", η μέθοδος που εμφανίζεται στην Λίστα 1 μετρά τον αριθμό των byte που καταλαμβάνονται από μια μήτρα n-by-n (δισδιάστατος πίνακας) διπλό.

Λίστα 1. Υπολογισμός αξιοποίησης μνήμης τύπου διπλού

 δημόσιο στατικό long getBytesUsingPrimitives (int n) {System.gc (); // αναγκαστική συλλογή απορριμμάτων long memStart = Runtime.getRuntime (). freeMemory (); διπλό [] [] α = νέο διπλό [n] [n]; // βάλτε μερικές τυχαίες τιμές στη μήτρα για (int i = 0; i <n; ++ i) {για (int j = 0; j <n; ++ j) a [i] [j] = Math. τυχαίος(); } long memEnd = Runtime.getRuntime (). freeMemory (); επιστροφή memStart - memEnd; } 

Τροποποιώντας τον κώδικα στην Λίστα 1 με τις προφανείς αλλαγές τύπου (δεν εμφανίζονται), μπορούμε επίσης να μετρήσουμε τον αριθμό των byte που καταλαμβάνονται από μια μήτρα n-by-n του Διπλό. Όταν δοκιμάζω αυτές τις δύο μεθόδους στον υπολογιστή μου χρησιμοποιώντας πίνακες 1000-1000, λαμβάνω τα αποτελέσματα που εμφανίζονται στον Πίνακα 1 παρακάτω. Όπως απεικονίζεται, η έκδοση για πρωτόγονο τύπο διπλό ισοδυναμεί με λίγο περισσότερο από 8 byte ανά καταχώριση στη μήτρα, περίπου αυτό που περίμενα. Ωστόσο, η έκδοση για τον τύπο αντικειμένου Διπλό απαιτούσε λίγο περισσότερο από 28 byte ανά καταχώριση στον πίνακα. Έτσι, στην περίπτωση αυτή, η χρήση της μνήμης του Διπλό είναι περισσότερο από τρεις φορές τη χρήση της μνήμης διπλό, το οποίο δεν πρέπει να αποτελεί έκπληξη για όποιον κατανοεί τη διάταξη της μνήμης που απεικονίζεται στο σχήμα 1 παραπάνω.

Πίνακας 1. Χρήση μνήμης διπλού έναντι διπλού

ΕκδοχήΣύνολο byteByte ανά καταχώριση
Χρησιμοποιώντας διπλό8,380,7688.381
Χρησιμοποιώντας Διπλό28,166,07228.166

Απόδοση χρόνου εκτέλεσης

Για να συγκρίνουμε τις επιδόσεις χρόνου εκτέλεσης για πρωτόγονα και αντικείμενα, χρειαζόμαστε έναν αλγόριθμο που κυριαρχείται από αριθμητικούς υπολογισμούς. Για αυτό το άρθρο επέλεξα τον πολλαπλασιασμό μήτρας και υπολογίζω το χρόνο που απαιτείται για τον πολλαπλασιασμό δύο πινάκων 1000 επί 1000. Κωδικοποίησα τον πολλαπλασιασμό μήτρας για διπλό με έναν απλό τρόπο όπως φαίνεται στην καταχώριση 2 παρακάτω. Ενώ μπορεί να υπάρχουν γρηγορότεροι τρόποι για την εφαρμογή πολλαπλασιασμού μήτρας (ίσως χρησιμοποιώντας ταυτόχρονη), αυτό το σημείο δεν είναι πραγματικά σχετικό με αυτό το άρθρο. Το μόνο που χρειάζομαι είναι κοινός κώδικας σε δύο παρόμοιες μεθόδους, μία χρησιμοποιώντας το πρωτόγονο διπλό και ένα χρησιμοποιώντας την τάξη περιτυλίγματος Διπλό. Ο κωδικός για τον πολλαπλασιασμό δύο πινάκων τύπου Διπλό είναι ακριβώς έτσι στη Λίστα 2 με τις προφανείς αλλαγές τύπου.

Λίστα 2. Πολλαπλασιάζοντας δύο πίνακες τύπου διπλού

 public static double [] [] multiply (double [] [] a, double [] [] b) {if (! checkArgs (a, b)) ρίξτε νέο IllegalArgumentException ("Οι μήτρες δεν είναι συμβατές για πολλαπλασιασμό"); int nRows = a.length; int nCols = b [0]. μήκος; διπλό [] [] αποτέλεσμα = νέο διπλό [nRows] [nCols]; για (int rowNum = 0; rowNum <nRows; ++ rowNum) {για (int colNum = 0; colNum <nCols; ++ colNum) {διπλό άθροισμα = 0,0; για (int i = 0; i <a [0] .length; ++ i) sum + = a [rowNum] [i] * b [i] [colNum]; αποτέλεσμα [rowNum] [colNum] = άθροισμα; }} αποτέλεσμα επιστροφής; } 

Έτρεξα τις δύο μεθόδους για να πολλαπλασιάσω δύο πίνακες 1000 με 1000 στον υπολογιστή μου αρκετές φορές και μέτρησα τα αποτελέσματα. Οι μέσοι χρόνοι φαίνονται στον Πίνακα 2. Έτσι, σε αυτήν την περίπτωση, η απόδοση χρόνου εκτέλεσης του διπλό είναι περισσότερο από τέσσερις φορές πιο γρήγορη από εκείνη του Διπλό. Αυτό είναι πάρα πολύ διαφορά για να το αγνοήσουμε.

Πίνακας 2. Απόδοση χρόνου εκτέλεσης διπλού έναντι διπλού

ΕκδοχήΔευτερόλεπτα
Χρησιμοποιώντας διπλό11.31
Χρησιμοποιώντας Διπλό48.48

Το σημείο αναφοράς SciMark 2.0

Μέχρι στιγμής έχω χρησιμοποιήσει το μοναδικό, απλό σημείο αναφοράς του πολλαπλασιασμού μήτρας για να δείξω ότι τα πρωτόγονα μπορούν να αποδώσουν σημαντικά μεγαλύτερη απόδοση υπολογιστών από τα αντικείμενα. Για να ενισχύσω τους ισχυρισμούς μου, θα χρησιμοποιήσω ένα πιο επιστημονικό σημείο αναφοράς. Το SciMark 2.0 είναι ένα σημείο αναφοράς Java για επιστημονικούς και αριθμητικούς υπολογιστές που διατίθενται από το Εθνικό Ινστιτούτο Προτύπων και Τεχνολογίας (NIST). Κατέβασα τον πηγαίο κώδικα για αυτό το σημείο αναφοράς και δημιούργησα δύο εκδόσεις, την αρχική έκδοση χρησιμοποιώντας πρωτόγονα και μια δεύτερη έκδοση χρησιμοποιώντας τάξεις περιτυλίγματος. Για τη δεύτερη έκδοση αντικατέστησα int με Ακέραιος αριθμός και διπλό με Διπλό για να αποκτήσετε το πλήρες αποτέλεσμα της χρήσης τάξεων περιτυλίγματος. Και οι δύο εκδόσεις είναι διαθέσιμες στον πηγαίο κώδικα για αυτό το άρθρο.

λήψη Java Benchmarking: Κατεβάστε τον πηγαίο κώδικα John I. Moore, Jr.

Το σημείο αναφοράς SciMark μετρά την απόδοση πολλών υπολογιστικών ρουτίνων και αναφέρει μια σύνθετη βαθμολογία σε κατά προσέγγιση Mflops (εκατομμύρια λειτουργίες κινητής υποδιαστολής ανά δευτερόλεπτο). Έτσι, μεγαλύτεροι αριθμοί είναι καλύτεροι για αυτό το σημείο αναφοράς. Ο Πίνακας 3 παρέχει τη μέση σύνθετη βαθμολογία από πολλές εκτελέσεις κάθε έκδοσης αυτού του δείκτη αναφοράς στον υπολογιστή μου. Όπως φαίνεται, οι επιδόσεις του χρόνου εκτέλεσης των δύο εκδόσεων του δείκτη αναφοράς SciMark 2.0 ήταν συνεπείς με τα αποτελέσματα πολλαπλασιασμού της μήτρας παραπάνω, καθώς η έκδοση με πρωτόγονα ήταν σχεδόν πέντε φορές ταχύτερη από την έκδοση χρησιμοποιώντας τάξεις wrapper.

Πίνακας 3. Απόδοση χρόνου εκτέλεσης του δείκτη αναφοράς SciMark

Έκδοση SciMarkΑπόδοση (Mflops)
Χρήση πρωτόγονων710.80
Χρήση τάξεων περιτυλίγματος143.73

Έχετε δει μερικές παραλλαγές προγραμμάτων Java που κάνουν αριθμητικούς υπολογισμούς, χρησιμοποιώντας τόσο ένα βασικό σημείο αναφοράς όσο και ένα πιο επιστημονικό. Αλλά πώς συγκρίνεται η Java με άλλες γλώσσες; Θα ολοκληρώσω με μια γρήγορη ματιά στο πώς συγκρίνεται η απόδοση της Java με εκείνη τριών άλλων γλωσσών προγραμματισμού: Scala, C ++ και JavaScript.

Σκάλα συγκριτικής αξιολόγησης

Το Scala είναι μια γλώσσα προγραμματισμού που τρέχει στο JVM και φαίνεται να κερδίζει δημοτικότητα. Το Scala έχει ένα ενοποιημένο σύστημα τύπου, που σημαίνει ότι δεν κάνει διάκριση μεταξύ πρωτόγονων και αντικειμένων. Σύμφωνα με τον Erik Osheim στην κατηγορία Numeric type της Scala (Pt. 1), η Scala χρησιμοποιεί πρωτόγονους τύπους όταν είναι δυνατόν, αλλά θα χρησιμοποιήσει αντικείμενα εάν είναι απαραίτητο. Ομοίως, η περιγραφή του Martin Odersky για το Scala's Arrays λέει ότι "... μια σειρά Scala Σειρά [Int] αντιπροσωπεύεται ως Java int [], ένα Σειρά [Διπλό] αντιπροσωπεύεται ως Java διπλό[] ..."

Αυτό σημαίνει λοιπόν ότι το ενοποιημένο σύστημα τύπου Scala θα έχει απόδοση χρόνου εκτέλεσης συγκρίσιμη με τους πρωτόγονους τύπους Java; Ας δούμε.