Γράφοντας γρήγορο pperl#

Τα άλλα κεφάλαια αυτού του οδηγού αφορούν την αφαίρεση κόστους. Αυτό αφορά την κατάκτηση των επιταχύνσεων που προσφέρει το pperl και που καμία άλλη Perl δεν έχει: το JIT που μεταγλωττίζει τους θερμούς βρόχους σε εγγενή κώδικα, και τον παραλληλοποιητή που απλώνει έναν επιλέξιμο βρόχο σε όλους τους πυρήνες της CPU. Και τα δύο είναι αυτόματα - δεν τα καλείτε, δεν τα σχολιάζετε, ούτε τα διαμορφώνετε. Αλλά ενεργοποιούνται μόνο σε κώδικα διαμορφωμένο με τον σωστό τρόπο, και μια μόνο λάθος τοποθετημένη λειτουργία μπορεί να απενεργοποιήσει αθόρυβα οποιοδήποτε από τα δύο.

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

Η μορφή που θέλει το JIT#

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

Ένας βρόχος που το JIT μεταγλωττίζει καθαρά:

my $sum = 0;
for my $i (1 .. 1_000_000) {
    $sum += $i * $i;
}

Η ακέραια και κινητής υποδιαστολής αριθμητική (+, -, *, /), οι συγκρίσεις (<, >, <=, >=, ==) και η ροή ελέγχου (if, last) μέσα στον βρόχο μεταγλωττίζονται όλα. Οι εμφωλευμένοι βρόχοι μεταγλωττίζονται επίσης, πολλά επίπεδα βαθιά, με κάθε επίπεδο συνδεδεμένο με το επόμενο. Η φυσική, ιδιωματική μορφή είναι η γρήγορη μορφή - δεν ξετυλίγετε, δεν βγάζετε έξω σε προσωρινές μεταβλητές, ούτε την παραμορφώνετε αλλιώς.

Τι σπρώχνει έναν βρόχο πίσω στον διερμηνευτή#

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

  • Κλήσεις υπορουτινών. Το JIT μεταγλωττίζει σώματα βρόχων, όχι πλαίσια κλήσης. Ένας βρόχος που καλεί μια sub σε κάθε επανάληψη τρέχει ερμηνευμένος - η επιβάρυνση κλήσης είναι ακριβώς αυτό που το JIT δεν μπορεί να αφαιρέσει. Αν η εργασία ανά επανάληψη είναι μικρή και θερμή, ενσωματώστε την μέσα στο σώμα του βρόχου αντί να τη μεταφέρετε σε μια sub.

  • Λειτουργίες regex. Η μηχανή regex έχει τον δικό της βελτιστοποιητή και το δικό της μονοπάτι· μια αντιστοιχία ή αντικατάσταση στο σώμα του βρόχου δεν μεταγλωττίζεται με JIT. Δείτε απόδοση regex για τη ρύθμιση αυτής της εργασίας με τους δικούς της όρους.

  • I/O. Ένα print, μια ανάγνωση αρχείου, μια κλήση συστήματος - η εργασία που περιορίζεται από το I/O δεν ωφελείται από την εγγενή μεταγλώττιση και δεν μεταγλωττίζεται.

  • Πρόσβαση σε σύνθετες δομές δεδομένων. Οι λειτουργίες hash και πίνακα με δυναμικά κλειδιά βρίσκονται εκτός του αριθμητικού μοντέλου του JIT.

Κανένα από αυτά δεν είναι σφάλμα· ο βρόχος απλώς τρέχει με τον ερμηνευμένο τρόπο. Αλλά αν ένας βρόχος που περιμένατε να είναι γρήγορος δεν είναι, τρέξτε τον με --no-jit έναντι της προεπιλογής (Μέτρηση): αν οι χρόνοι ταιριάζουν, το JIT τον απέρριπτε ήδη, και ένα από τα παραπάνω είναι ο λόγος.

Κρατήστε τις συμβολοσειρές έξω από τους αριθμητικούς βρόχους#

Το JIT όντως χειρίζεται μία λειτουργία συμβολοσειράς - την .= συνένωση-ανάθεση, και το άδειασμα μιας συμβολοσειράς με $x = "" - μέσω κλήσεων πίσω στο runtime. Έτσι ένας βρόχος χτισίματος συμβολοσειρών εξακολουθεί να μεταγλωττίζεται. Αλλά αυτή είναι η μία περίπτωση όπου η μορφή του βρόχου αλλάζει ποιοι βελτιστοποιητές εφαρμόζονται, και είναι ο πολυτιμότερος κανόνας αυτού του κεφαλαίου.

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

Η πρακτική συνέπεια: μια αριθμητική αναγωγή που θα παραλληλοποιούνταν χάνει αυτόν τον παραλληλισμό αν χτίσετε μια συμβολοσειρά στον ίδιο βρόχο.

# Parallelizable: pure numeric reduction.
my $total = 0;
for my $i (0 .. $n) {
    $total += $i * $i;
}

# Same arithmetic, but the string build demotes it to a
# single-core compiled loop - the parallel speedup is forfeited.
my $report = "";
my $total  = 0;
for my $i (0 .. $n) {
    $total  += $i * $i;
    $report .= "$i ";
}

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

Η μορφή που θέλει ο παραλληλοποιητής#

Όταν η παραλληλοποίηση είναι ενεργοποιημένη - είναι ενεργή εξ ορισμού· η --no-parallel την απενεργοποιεί - ένας μεταγλωττισμένος με JIT βρόχος αποστέλλεται σε όλα τα νήματα αν η ανάλυση μπορεί να αποδείξει ότι είναι ασφαλής. Δύο συνθήκες έχουν τη μεγαλύτερη βαρύτητα:

Μια αναγωγή διαμορφωμένη ως συσσώρευση-χωρίς-μηδενισμό. Ο παραλληλοποιητής αναγνωρίζει μια μεταβλητή αναγωγής βλέποντάς την να συσσωρεύεται και να μη μηδενίζεται ποτέ μέσα στον βρόχο. Δηλώστε τον συσσωρευτή πριν τον βρόχο, χρησιμοποιήστε την απλή μορφή +=, και ποτέ μην τον επαναρχικοποιείτε μέσα στο σώμα:

my $sum = 0;                    # declared outside the loop
for my $x (@data) {
    $sum += score($x);          # accumulate only → recognised as a reduction
}

Η επαναρχικοποίηση της $sum μέσα στον βρόχο, ή η παρεμβολή ενός μηδενισμού, ματαιώνει την αναγνώριση (η ανάλυση αφαιρεί τα μοτίβα μηδενισμού από τα μοτίβα συσσώρευσης) και ο βρόχος τρέχει σειριακά. Πλήρης λεπτομέρεια στην ανίχνευση αναγωγής.

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

  • I/O (print, open, αναγνώσεις αρχείων)

  • Εγγραφές σε μεταβλητές πακέτου/καθολικές

  • Κλήσεις υπορουτινών που δεν έχουν αποδειχθεί καθαρές

  • Λειτουργίες regex με παρενέργειες (s///)

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

Ενσωματωμένες συναρτήσεις που παραλληλοποιούνται#

Οι map και grep με καθαρά callbacks πάνω σε μια αρκετά μεγάλη συλλογή μπορούν να τρέξουν παράλληλα, με τη σειρά εξόδου διατηρημένη:

my @out      = map  { expensive($_) } @big;
my @filtered = grep { costly_test($_) } @big;

Ισχύουν οι ίδιες πύλες: το callback πρέπει να είναι ελεύθερο από παρενέργειες και κοινόχρηστη μεταβλητή κατάσταση, και η συλλογή πρέπει να ξεπερνά το κατώφλι παραλληλοποίησης (ρυθμιζόμενο με --parallel-threshold). Ένας καθαρός μετασχηματισμός πάνω σε έναν μεγάλο πίνακα είναι η ευκολότερη παράλληλη νίκη που προσφέρει το pperl - κανένας βρόχος προς διαμόρφωση, απλώς ένα καθαρό callback.

Όταν η εργασία δεν είναι βρόχος#

Δεν είναι κάθε πρόγραμμα ένας αριθμητικός βρόχος, και το JIT και ο παραλληλοποιητής δεν έχουν τίποτε να προσφέρουν σε κώδικα που περιορίζεται από κλήσεις, από το I/O, ή που έχει μορφή συμβολοσειράς. Για αυτόν τον κώδικα ισχύει πλήρως το υπόλοιπο αυτού του οδηγού:

  • Επιλέξτε native ενσωματωμένες συναρτήσεις αντί για χειρόγραφους βρόχους όπου υπάρχουν - το List::Util τρέχει με ταχύτητα ενσωματωμένης συνάρτησης.

  • Εφαρμόστε τους κλασικούς ιδιωματισμούς που εξακολουθούν να ισχύουν από τους Ιδιωματισμούς: βγάλτε έξω τα αμετάβλητα, αποφύγετε αντίγραφα, προμεταγλωττίστε regexp.

  • Μετασχηματίστε τις ακριβές ταξινομήσεις (Ταξινόμηση).

  • Καλέστε τη C μέσω του FFI μόνο όταν η συχνότητα κλήσης είναι χαμηλή - το κόστος μαρσαλαρίσματος ανά κλήση το καθιστά λάθος εργαλείο μέσα σε έναν θερμό εσωτερικό βρόχο.

Μια λίστα ελέγχου για έναν θερμό βρόχο#

  1. Είναι το σώμα αριθμητική και σύγκριση πάνω σε αριθμητικές μεταβλητές; Αν ναι, το JIT το μεταγλωττίζει. Αν όχι, ο παράγοντας αποκλεισμού είναι μια κλήση, ένα regex, I/O, ή δυναμική πρόσβαση δεδομένων - αντιμετωπίστε το αυτό πρώτα.

  2. Υπάρχει λειτουργία συμβολοσειράς στον βρόχο; Αν ναι, και ο βρόχος είναι κατά τα άλλα μια αριθμητική αναγωγή, χωρίστε έξω την εργασία με συμβολοσειρές ώστε ο αριθμητικός βρόχος να μπορεί να παραλληλοποιηθεί.

  3. Είναι ο συσσωρευτής δηλωμένος έξω από τον βρόχο και μόνο συσσωρεύεται, ποτέ δεν μηδενίζεται; Αυτό είναι που τον κάνει αναγνωρισμένη αναγωγή.

  4. Είναι το σώμα ελεύθερο από I/O, καθολικές εγγραφές και μη καθαρές κλήσεις; Αυτό είναι που επιτρέπει στον βρόχο να παραλληλοποιηθεί.

  5. Μετρήστε την προεπιλογή έναντι --no-jit έναντι --no-parallel (Μέτρηση) για να επιβεβαιώσετε ποιος βελτιστοποιητής εμπλέκεται.

Δείτε επίσης#

  • Μεταγλώττιση JIT - η σωλήνωση μεταγλώττισης, το μοντέλο τύπου μεταβλητών, η αποθήκευση σε cache.

  • Παράλληλη εκτέλεση - η ανάλυση αναγωγής, η πύλη παρενεργειών, οι έλεγχοι νημάτων.

  • Μέτρηση - πώς να επιβεβαιώσετε ότι το JIT και ο παραλληλοποιητής πράγματι εμπλέκονται στον κώδικά σας.

  • Ιδιωματισμοί - ποιες κλασικές βελτιστοποιήσεις έχουν ακόμη σημασία και ποιες χειρίζεται ήδη ο μεταγλωττιστής.

  • Ταξινόμηση - το ένα κοινό θερμό μονοπάτι που το JIT δεν φτάνει.