Γράφοντας γρήγορο 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) μέσα στον βρόχο μεταγλωττίζονται όλα. Οι εμφωλευμένοι βρόχοι μεταγλωττίζονται επίσης, πολλά επίπεδα βαθιά, με κάθε επίπεδο συνδεδεμένο με το επόμενο. Η φυσική, ιδιωματική μορφή είναι η γρήγορη μορφή - δεν ξετυλίγετε, δεν βγάζετε έξω σε προσωρινές μεταβλητές, ούτε την παραμορφώνετε αλλιώς.

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

The JIT declines a loop whose body does work it cannot prove it can reproduce exactly. When it declines, the loop runs on the interpreter - at interpreter speed, which is still fast for non-numeric work, but without the native-code multiplier. The coverage is broad (arithmetic, strings, array and hash elements, push/pop, map/grep, numeric sort, join/split, regex matches and constant substitutions, calls to small pure subs, and the common Scalar::Util/List::Util operations - blessed, reftype, weaken, sum, min, max and friends), so the practical disqualifiers are fewer than you might expect:

  • Calls to subs with side effects. A call compiles when the sub’s body is a my (...) = @_; prelude plus one pure numeric expression - including recursion. A sub that touches globals, does I/O, uses return, or takes references to @_ declines the loop. Keep hot helpers small and pure and the call costs almost nothing; factoring work OUT of a sub is no longer the advice.

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

  • Regex beyond the compiled forms. Scalar matches and s/pat/constant/ (including /g) compile; s///e, scalar m//g, and patterns with embedded code do not. See regex performance for tuning the engine’s own work.

  • Magic in any form. Tied variables, overloaded objects, local, taint mode.

Κανένα από αυτά δεν είναι σφάλμα· ο βρόχος απλώς τρέχει με τον ερμηνευμένο τρόπο. Αλλά αν ένας βρόχος που περιμένατε να είναι γρήγορος δεν είναι, τρέξτε τον με --no-jit έναντι της προεπιλογής (Μέτρηση): αν οι χρόνοι ταιριάζουν, το JIT τον απέρριπτε ήδη. Ορίστε PPERL_JIT_LOG=/tmp/jit.log και διαβάστε τις γραμμές compile skip reason=... για να δείτε ακριβώς ποια πράξη ευθύνεται (Μεταγλώττιση 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 and grep compile too#

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

my @out      = map  { $_ * $k + 1 } @data;
my @filtered = grep { $_ % 7 }      @data;

The block must be a side-effect-free expression over $_, loop variables, and constants; mutating $_ (which writes through to the source array) or producing a boolean as the map VALUE declines. Like all array work, compiled map/grep runs sequentially - array state gates parallel dispatch off - but the native-loop speedup over the interpreted op machinery is itself several-fold.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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