diff --git a/README.el.md b/README.el.md new file mode 100644 index 0000000..9d91b9f --- /dev/null +++ b/README.el.md @@ -0,0 +1,19 @@ +Καλώς ήρθατε στη Σχολή Γλώσσας Assembly της FFmpeg. Έχετε κάνει το πρώτο βήμα σε ένα από τα πιο ενδιαφέροντα, απαιτητικά και ικανοποιητικά ταξίδια στον κόσμο του προγραμματισμού. Αυτά τα μαθήματα θα σας δώσουν τις βασικές γνώσεις για τον τρόπο με τον οποίο γράφεται η γλώσσα Assembly στο FFmpeg και θα σας ανοίξει τα μάτια για το τι πραγματικά συμβαίνει στον υπολογιστή σας. + +**Απαιτούμενες γνώσεις** + +* Γνώση της γλώσσας C, ιδίως των δεικτών. Αν δεν γνωρίζετε C, μελετήστε το βιβλίο [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language) +* Μαθηματικά Λυκείου (βαθμωτό έναντι διανυσματικού, πρόσθεση, πολλαπλασιασμός κ.λπ.) + +**Μαθήματα** + +Σε αυτό το αποθετήριο Git υπάρχουν μαθήματα και εργασίες (δεν έχουν αναρτηθεί ακόμη) που αντιστοιχούν σε κάθε μάθημα. Μέχρι το τέλος των μαθημάτων θα είστε σε θέση να συνεισφέρετε στο FFmpeg. + +Ένας διακομιστής discord είναι διαθέσιμος για να απαντήσει σε ερωτήσεις: +https://discord.com/invite/Ks5MhUhqfB + +**Μεταφράσεις** + +* [Français](./README.fr.md) +* [Spanish](./README.es.md) +* [Ελληνικά](./README.el.md) \ No newline at end of file diff --git a/README.es.md b/README.es.md index 7c9a91a..1be46ab 100644 --- a/README.es.md +++ b/README.es.md @@ -15,4 +15,5 @@ https://discord.com/invite/Ks5MhUhqfB **Traducciones** * [English](./README.md) -* [Français](./README.fr.md) \ No newline at end of file +* [Français](./README.fr.md) +* [Griego](./README.el.md) \ No newline at end of file diff --git a/README.fr.md b/README.fr.md index b9b1fa8..0f997a0 100644 --- a/README.fr.md +++ b/README.fr.md @@ -14,5 +14,6 @@ https://discord.com/invite/Ks5MhUhqfB **Traductions** -* [English](./README.md) -* [Spanish](./README.es.md) \ No newline at end of file +* [Anglais](./README.md) +* [Espagnol](./README.es.md) +* [Grec](./README.el.md) \ No newline at end of file diff --git a/lesson_01/index.el.md b/lesson_01/index.el.md new file mode 100644 index 0000000..b6afad5 --- /dev/null +++ b/lesson_01/index.el.md @@ -0,0 +1,210 @@ +**Μάθημα Πρώτο Γλώσσας Assembly του FFmpeg** + +**Εισαγωγή** + +Καλώς ήρθατε στη Σχολή Γλώσσας Assembly της FFmpeg. Έχετε κάνει το πρώτο βήμα σε ένα από τα πιο ενδιαφέροντα, απαιτητικά και ικανοποιητικά ταξίδια στον κόσμο του προγραμματισμού. Αυτά τα μαθήματα θα σας δώσουν τις βασικές γνώσεις για τον τρόπο με τον οποίο γράφεται η γλώσσα Assembly στο FFmpeg και θα σας ανοίξει τα μάτια για το τι πραγματικά συμβαίνει στον υπολογιστή σας. + +**Απαιτούμενες γνώσεις** + +* Γνώση της γλώσσας C, ιδίως των δεικτών. Αν δεν γνωρίζετε C, μελετήστε το βιβλίο [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language) +* Μαθηματικά Λυκείου (βαθμωτό έναντι διανυσματικού, πρόσθεση, πολλαπλασιασμός κ.λπ.) + +**Τι είναι η γλώσσα Assembly;** + +Η γλώσσα Assembly είναι μια γλώσσα προγραμματισμού στην οποία γράφετε κώδικα που αντιστοιχεί άμεσα στις εντολές που επεξεργάζεται μια CPU. Η γλώσσα Assembly που είναι αναγνώσιμη από τον άνθρωπο, όπως υποδηλώνει το όνομά της, *συναρμολογείται* σε δυαδικά δεδομένα, γνωστά ως *κώδικας μηχανής*, τα οποία μπορεί να κατανοήσει η CPU. Μπορεί να δείτε τον κώδικα γλώσσας Assembly να αναφέρεται ως «assembly» ή «asm» για συντομία. + +Η συντριπτική πλειοψηφία του κώδικα assembly στο FFmpeg είναι αυτό που είναι γνωστό ως *SIMD, Single Instruction Multiple Data*. Το SIMD αναφέρεται μερικές φορές ως προγραμματισμός διανυσμάτων. Αυτό σημαίνει ότι μια συγκεκριμένη εντολή λειτουργεί σε πολλαπλά στοιχεία δεδομένων ταυτόχρονα. Οι περισσότερες γλώσσες προγραμματισμού λειτουργούν σε ένα στοιχείο δεδομένων κάθε φορά, κάτι που είναι γνωστό ως βαθμωτός προγραμματισμός (scalar programming). + +Όπως ίσως μαντέψατε, το SIMD προσφέρεται για την επεξεργασία εικόνων, βίντεο και ήχου, τα οποία έχουν πολλά δεδομένα διατεταγμένα διαδοχικά στη μνήμη. Υπάρχουν εξειδικευμένες εντολές διαθέσιμες στην Κεντρική Μονάδα Επεξεργασίας (CPU) για να μας βοηθήσουν να επεξεργαστούμε διαδοχικά δεδομένα. + +Στο FFmpeg, θα δείτε τους όρους «assembly function» (συνάρτηση assembly), «SIMD» και «vector(ise)» (διανυσματοποίηση) να χρησιμοποιούνται εναλλακτικά. Όλοι αναφέρονται στο ίδιο πράγμα: Στη χειροκίνητη συγγραφή μιας συνάρτησης σε γλώσσα assembly για την ταυτόχρονη επεξεργασία πολλαπλών στοιχείων δεδομένων. Ορισμένα έργα (projects) μπορεί επίσης να αναφέρονται σε αυτά ως «assembly kernels» (πυρήνες assembly). + +Όλα αυτά μπορεί να ακούγονται περίπλοκα, αλλά είναι σημαντικό να θυμάστε ότι στο FFmpeg, μαθητές λυκείου έχουν γράψει κώδικα assembly. Όπως με τα πάντα, η εκμάθηση είναι 50% ορολογία και 50% πραγματική μάθηση. + +**Γιατί γράφουμε σε γλώσσα assembly;** +Για να κάνουμε την επεξεργασία πολυμέσων γρήγορη. Είναι πολύ συνηθισμένο να επιτυγχάνεται βελτίωση ταχύτητας 10 φορές ή και περισσότερο από τη συγγραφή κώδικα assembly, κάτι που είναι ιδιαίτερα σημαντικό όταν θέλουμε να αναπαράγουμε βίντεο σε πραγματικό χρόνο χωρίς διακοπές (stuttering). Επίσης, εξοικονομεί ενέργεια και παρατείνει τη διάρκεια ζωής της μπαταρίας. Αξίζει να σημειωθεί ότι οι συναρτήσεις κωδικοποίησης και αποκωδικοποίησης βίντεο είναι μερικές από τις πιο χρησιμοποιημένες συναρτήσεις στον κόσμο, τόσο από τελικούς χρήστες όσο και από μεγάλες εταιρείες στα κέντρα δεδομένων (datacenters) τους. Έτσι, ακόμη και μια μικρή βελτίωση αθροίζεται γρήγορα. + +Θα δείτε συχνά, στο διαδίκτυο, ανθρώπους να χρησιμοποιούν *intrinsics* (ενδογενείς συναρτήσεις), που είναι συναρτήσεις παρόμοιες με τη C οι οποίες αντιστοιχούν σε εντολές assembly για να επιτρέψουν ταχύτερη ανάπτυξη. Στο FFmpeg δεν χρησιμοποιούμε intrinsics, αλλά αντ' αυτού γράφουμε κώδικα assembly χειροκίνητα. Αυτός είναι ένας τομέας διαφωνίας, αλλά τα intrinsics είναι συνήθως περίπου 10-15% πιο αργά από τον χειρόγραφο κώδικα assembly (οι υποστηρικτές των intrinsics θα διαφωνούσαν), ανάλογα με τον μεταγλωττιστή (compiler). Για το FFmpeg, κάθε κομμάτι επιπλέον απόδοσης βοηθάει, γι' αυτό και γράφουμε απευθείας σε κώδικα assembly. Υπάρχει επίσης το επιχείρημα ότι τα intrinsics είναι δύσκολο να διαβαστούν λόγω της χρήσης της «Ουγγρικής Σημειογραφίας» [Hungarian Notation](https://en.wikipedia.org/wiki/Hungarian_notation). + +Μπορεί επίσης να δείτε *ενσωματωμένη assembly* (inline assembly) (δηλαδή χωρίς τη χρήση intrinsics) να παραμένει σε μερικά σημεία στο FFmpeg για ιστορικούς λόγους, ή σε έργα όπως ο πυρήνας του Linux (Linux Kernel) λόγω πολύ συγκεκριμένων περιπτώσεων χρήσης εκεί. Αυτό συμβαίνει όταν ο κώδικας assembly δεν βρίσκεται σε ξεχωριστό αρχείο, αλλά είναι γραμμένος ενσωματωμένα με τον κώδικα C. Η επικρατούσα άποψη σε έργα όπως το FFmpeg είναι ότι αυτός ο κώδικας είναι δύσκολος στην ανάγνωση, δεν υποστηρίζεται ευρέως από τους μεταγλωττιστές (compilers) και δεν είναι συντηρήσιμος (unmaintainable). + +Τέλος, θα δείτε πολλούς αυτοαποκαλούμενους ειδικούς στο διαδίκτυο να λένε ότι τίποτα από όλα αυτά δεν είναι απαραίτητο και ότι ο μεταγλωττιστής (compiler) μπορεί να κάνει όλη αυτή τη «διανυσματοποίηση» (vectorisation) για εσάς. Τουλάχιστον για εκπαιδευτικούς σκοπούς, αγνοήστε τους: πρόσφατες δοκιμές για παράδειγμα στο [the dav1d project](https://www.videolan.org/projects/dav1d.html) έδειξαν περίπου 2 φορές επιτάχυνση από αυτήν την αυτόματη διανυσματοποίηση, ενώ οι χειρόγραφες εκδόσεις μπορούσαν να φτάσουν τις 8 φορές. + +**Εκδοχές (Flavours) της γλώσσας Assembly** +Αυτά τα μαθήματα θα επικεντρωθούν στη γλώσσα assembly x86 64-bit. Αυτή είναι επίσης γνωστή ως amd64, αν και εξακολουθεί να λειτουργεί σε επεξεργαστές (CPU) της Intel. Υπάρχουν και άλλοι τύποι assembly για άλλους επεξεργαστές όπως οι ARM και RISC-V και πιθανώς στο μέλλον αυτά τα μαθήματα θα επεκταθούν για να τους καλύψουν. +Υπάρχουν δύο εκδοχές σύνταξης της x86 assembly που θα δείτε στο διαδίκτυο: η AT&T και η Intel. Η σύνταξη AT&T είναι παλαιότερη και πιο δύσκολη στην ανάγνωση σε σύγκριση με τη σύνταξη της Intel. Επομένως, θα χρησιμοποιήσουμε τη σύνταξη της Intel. + +**Υποστηρικτικό υλικό** +Μπορεί να σας εκπλήξει το γεγονός ότι βιβλία ή διαδικτυακοί πόροι όπως το Stack Overflow δεν είναι ιδιαίτερα χρήσιμα ως αναφορές. Αυτό οφείλεται εν μέρει στην επιλογή μας να χρησιμοποιούμε χειρόγραφη assembly με σύνταξη Intel. Αλλά και επειδή πολλοί διαδικτυακοί πόροι επικεντρώνονται στον προγραμματισμό λειτουργικών συστημάτων ή στον προγραμματισμό υλικού (hardware), χρησιμοποιώντας συνήθως κώδικα που δεν είναι SIMD. Η assembly του FFmpeg είναι ιδιαίτερα εστιασμένη στην επεξεργασία εικόνας υψηλής απόδοσης, και όπως θα δείτε, αποτελεί μια ιδιαίτερα μοναδική προσέγγιση στον προγραμματισμό assembly. Παρόλα αυτά, είναι εύκολο να κατανοήσετε άλλες περιπτώσεις χρήσης της assembly μόλις ολοκληρώσετε αυτά τα μαθήματα. + +Πολλά βιβλία εμβαθύνουν σε πολλές λεπτομέρειες της αρχιτεκτονικής των υπολογιστών πριν διδάξουν assembly. Αυτό είναι καλό αν αυτό είναι που θέλετε να μάθετε, αλλά από τη δική μας σκοπιά, είναι σαν να μελετάτε τις μηχανές πριν μάθετε να οδηγείτε αυτοκίνητο. + +Παρόλα αυτά, τα διαγράμματα στα μεταγενέστερα μέρη του βιβλίου «The Art of 64-bit assembly» που δείχνουν τις εντολές SIMD και τη συμπεριφορά τους σε οπτική μορφή είναι χρήσιμα: [https://artofasm.randallhyde.com/](https://artofasm.randallhyde.com/) + +Ένας διακομιστής Discord (server) είναι διαθέσιμος για να απαντήσει σε ερωτήσεις: [https://discord.com/invite/Ks5MhUhqfB](https://discord.com/invite/Ks5MhUhqfB) + +**Καταχωρητές (Registers)** +Οι καταχωρητές είναι περιοχές στην ΚΜΕ (CPU) όπου τα δεδομένα μπορούν να υποστούν επεξεργασία. Οι ΚΜΕ δεν λειτουργούν απευθείας πάνω στη μνήμη, αλλά αντ' αυτού τα δεδομένα φορτώνονται στους καταχωρητές, επεξεργάζονται και εγγράφονται πίσω στη μνήμη. Στη γλώσσα assembly, γενικά, δεν μπορείτε να αντιγράψετε απευθείας δεδομένα από μια θέση μνήμης σε μια άλλη χωρίς πρώτα να περάσετε αυτά τα δεδομένα μέσω ενός καταχωρητή. + +**Καταχωρητές Γενικού Σκοπού (General Purpose Registers)** +Ο πρώτος τύπος καταχωρητή είναι αυτός που είναι γνωστός ως Καταχωρητής Γενικού Σκοπού (GPR). Οι GPRs αναφέρονται ως γενικού σκοπού επειδή μπορούν να περιέχουν είτε δεδομένα, στην περίπτωση αυτή μια τιμή έως 64-bit, είτε μια διεύθυνση μνήμης (έναν δείκτη). Μια τιμή σε έναν GPR μπορεί να επεξεργαστεί μέσω πράξεων όπως πρόσθεση, πολλαπλασιασμός, μετατόπιση κ.λπ. + +Στα περισσότερα βιβλία για assembly, υπάρχουν ολόκληρα κεφάλαια αφιερωμένα στις λεπτές αποχρώσεις των GPRs, το ιστορικό τους υπόβαθρο κ.λπ. Αυτό συμβαίνει επειδή οι GPRs είναι σημαντικοί όσον αφορά τον προγραμματισμό λειτουργικών συστημάτων, την αντίστροφη μηχανική (reverse engineering), κ.λπ. Στον κώδικα assembly που γράφεται στο FFmpeg, οι GPRs μοιάζουν περισσότερο με σκαλωσιές (scaffolding) και τις περισσότερες φορές οι πολυπλοκότητές τους δεν χρειάζονται και αφαιρούνται (abstracted away). + +**Διανυσματικοί καταχωρητές (Vector registers)** +Οι διανυσματικοί (SIMD) καταχωρητές, όπως υποδηλώνει το όνομά τους, περιέχουν πολλαπλά στοιχεία δεδομένων. Υπάρχουν διάφοροι τύποι διανυσματικών καταχωρητών: + +* καταχωρητές mm - καταχωρητές MMX, μεγέθους 64-bit, ιστορικοί και δεν χρησιμοποιούνται πλέον πολύ +* καταχωρητές xmm - καταχωρητές XMM, μεγέθους 128-bit, ευρέως διαθέσιμοι +* καταχωρητές ymm - καταχωρητές YMM, μεγέθους 256-bit, με μερικές περιπλοκές κατά τη χρήση τους +* καταχωρητές zmm - καταχωρητές ZMM, μεγέθους 512-bit, περιορισμένης διαθεσιμότητας + +Οι περισσότεροι υπολογισμοί στη συμπίεση και αποσυμπίεση βίντεο βασίζονται σε ακέραιους αριθμούς, οπότε θα παραμείνουμε σε αυτό. Ακολουθεί ένα παράδειγμα 16 byte σε έναν καταχωρητή xmm: + +| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +Αλλά θα μπορούσε να είναι οκτώ λέξεις (words) (ακέραιοι 16-bit) + +| a | b | c | d | e | f | g | h | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +Ή τέσσερις διπλές λέξεις (double words) (ακέραιοι 32-bit) + +| a | b | c | d | +| :---- | :---- | :---- | :---- | + +Ή δύο τετραπλές λέξεις (quadwords) (ακέραιοι 64-bit): + +| a | b | +| :---- | :---- | + +Για να ανακεφαλαιώσουμε: + +* **b**ytes (μπάιτ) - δεδομένα 8-bit +* **w**ords (λέξεις) - δεδομένα 16-bit +* **d**oublewords (διπλές λέξεις) - δεδομένα 32-bit +* **q**uadwords (τετραπλές λέξεις) - δεδομένα 64-bit +* **d**ouble **q**uadwords (διπλές τετραπλές λέξεις) - δεδομένα 128-bit + +Οι έντονοι χαρακτήρες θα είναι σημαντικοί αργότερα. + +**Συμπερίληψη του x86inc.asm** +Θα δείτε σε πολλά παραδείγματα ότι συμπεριλαμβάνουμε το αρχείο x86inc.asm. Το X86inc.asm είναι ένα ελαφρύ επίπεδο αφαίρεσης (lightweight abstraction layer) που χρησιμοποιείται στα FFmpeg, x264 και dav1d για να κάνει τη ζωή ενός προγραμματιστή assembly ευκολότερη. Βοηθά με πολλούς τρόπους, αλλά για αρχή, ένα από τα χρήσιμα πράγματα που κάνει είναι ότι ονομάζει τους GPRs, r0, r1, r2. Αυτό σημαίνει ότι δεν χρειάζεται να θυμάστε κανένα όνομα καταχωρητή. Όπως αναφέρθηκε προηγουμένως, οι GPRs είναι γενικά απλώς σκαλωσιές (scaffolding), οπότε αυτό κάνει τη ζωή πολύ πιο εύκολη. + +**Ένα απλό απόσπασμα κώδικα βαθμωτής assembly** + +Ας δούμε ένα απλό (και πολύ τεχνητό) απόσπασμα κώδικα βαθμωτής assembly (κώδικας assembly που λειτουργεί σε μεμονωμένα στοιχεία δεδομένων, ένα κάθε φορά, μέσα σε κάθε εντολή) για να δούμε τι συμβαίνει: + +```assembly +mov r0q, 3 +inc r0q +dec r0q +imul r0q, 5 +``` + +Στην πρώτη γραμμή, η *άμεση τιμή* (immediate value) 3 (μια τιμή που αποθηκεύεται απευθείας στον ίδιο τον κώδικα assembly σε αντίθεση με μια τιμή που ανακτάται από τη μνήμη) αποθηκεύεται στον καταχωρητή r0 ως τετραπλή λέξη (quadword). Σημειώστε ότι στη σύνταξη της Intel, ο τελεστέος προέλευσης (source operand) (η τιμή ή η θέση που παρέχει τα δεδομένα, που βρίσκεται στα δεξιά) μεταφέρεται στον τελεστέο προορισμού (destination operand) (η θέση που λαμβάνει τα δεδομένα, που βρίσκεται στα αριστερά), παρόμοια με τη συμπεριφορά της memcpy. Μπορείτε επίσης να το διαβάσετε ως «r0q = 3», αφού η σειρά είναι η ίδια. Το επίθημα «q» του r0 υποδηλώνει ότι ο καταχωρητής χρησιμοποιείται ως τετραπλή λέξη. Η εντολή inc αυξάνει την τιμή ώστε το r0q να περιέχει 4, η dec μειώνει την τιμή ξανά στο 3. Η imul πολλαπλασιάζει την τιμή με το 5. Έτσι, στο τέλος, το r0q περιέχει 15. + +Σημειώστε ότι οι αναγνώσιμες από τον άνθρωπο εντολές όπως mov και inc, οι οποίες μετατρέπονται σε κώδικα μηχανής από τον assembler, είναι γνωστές ως μνημονικά (mnemonics). Μπορεί να δείτε στο διαδίκτυο και σε βιβλία τα μνημονικά να αναπαρίστανται με κεφαλαία γράμματα όπως MOV και INC, αλλά είναι τα ίδια με τις πεζές εκδόσεις. Στο FFmpeg, χρησιμοποιούμε πεζά μνημονικά και κρατάμε τα κεφαλαία για τις μακροεντολές (macros). + +**Κατανόηση μιας βασικής διανυσματικής συνάρτησης** + +Αυτή είναι η πρώτη μας συνάρτηση SIMD: + +```assembly +%include "x86inc.asm" + +SECTION .text + +;static void add_values(uint8_t *src, const uint8_t *src2) +INIT_XMM sse2 +cglobal add_values, 2, 2, 2, src, src2 + movu m0, [srcq] + movu m1, [src2q] + + paddb m0, m1 + + movu [srcq], m0 + + RET +``` + +Ας το δούμε γραμμή προς γραμμή: + +```assembly +%include "x86inc.asm" +``` + +Αυτή είναι μια «επικεφαλίδα» (header) που αναπτύχθηκε στις κοινότητες των x264, FFmpeg και dav1d για να παρέχει βοηθητικές ρουτίνες (helpers), προκαθορισμένα ονόματα και μακροεντολές (macros) (όπως η cglobal παρακάτω) για την απλοποίηση της συγγραφής κώδικα assembly. + +```assembly +SECTION .text +``` + +Αυτό υποδηλώνει το τμήμα όπου τοποθετείται ο κώδικας που θέλετε να εκτελεστεί. Αυτό έρχεται σε αντίθεση με το τμήμα .data, όπου μπορείτε να τοποθετήσετε σταθερά δεδομένα. + +```assembly +;static void add_values(uint8_t *src, const uint8_t *src2) +INIT_XMM sse2 +``` + +Η πρώτη γραμμή είναι ένα σχόλιο (το ερωτηματικό «;» στην asm είναι σαν το «//» στη C) που δείχνει πώς φαίνεται το όρισμα της συνάρτησης στη C. Η δεύτερη γραμμή δείχνει πώς αρχικοποιούμε τη συνάρτηση για να χρησιμοποιήσει καταχωρητές XMM, χρησιμοποιώντας το σύνολο εντολών sse2. Αυτό συμβαίνει επειδή η paddb είναι μια εντολή sse2. Θα καλύψουμε το sse2 με περισσότερες λεπτομέρειες στο επόμενο μάθημα. + +```assembly +cglobal add_values, 2, 2, 2, src, src2 +``` + +Αυτή είναι μια σημαντική γραμμή καθώς ορίζει μια συνάρτηση C με το όνομα «add_values». + +Ας εξετάσουμε κάθε στοιχείο ένα προς ένα: + +* Η επόμενη παράμετρος δείχνει ότι έχει δύο ορίσματα συνάρτησης. +* Η παράμετρος μετά από αυτή δείχνει ότι θα χρησιμοποιήσουμε δύο GPRs για τα ορίσματα. Σε ορισμένες περιπτώσεις μπορεί να θέλουμε να χρησιμοποιήσουμε περισσότερους GPRs, οπότε πρέπει να πούμε στο x86util ότι χρειαζόμαστε περισσότερους. +* Η παράμετρος μετά από αυτή λέει στο x86util πόσους καταχωρητές XMM θα χρησιμοποιήσουμε. +* Οι επόμενες δύο παράμετροι είναι ετικέτες για τα ορίσματα της συνάρτησης. + +Αξίζει να σημειωθεί ότι παλαιότερος κώδικας μπορεί να μην έχει ετικέτες για τα ορίσματα της συνάρτησης, αλλά αντ' αυτού να απευθύνεται απευθείας στους GPRs χρησιμοποιώντας r0, r1 κ.λπ. + +```assembly + movu m0, [srcq] + movu m1, [src2q] +``` + +Η movu είναι συντομογραφία για την movdqu (move double quad unaligned - μετακίνηση μη ευθυγραμμισμένης διπλής τετραπλής λέξης). Η ευθυγράμμιση (Alignment) θα καλυφθεί σε άλλο μάθημα, αλλά προς το παρόν η movu μπορεί να θεωρηθεί ως μια μετακίνηση 128-bit από το [srcq]. Στην περίπτωση της mov, οι αγκύλες σημαίνουν ότι η διεύθυνση στο [srcq] αποαναφέρεται (dereferenced), το ισοδύναμο του **src στη C*. Αυτό είναι γνωστό ως φόρτωση (load). Σημειώστε ότι το επίθημα «q» αναφέρεται στο μέγεθος του δείκτη *(*δηλαδή στη C αντιπροσωπεύει το *sizeof(*src) == 8 σε συστήματα 64-bit, και η x86asm είναι αρκετά έξυπνη ώστε να χρησιμοποιεί 32-bit σε συστήματα 32-bit) αλλά η υποκείμενη φόρτωση είναι 128-bit. + +Σημειώστε ότι δεν αναφερόμαστε στους διανυσματικούς καταχωρητές με το πλήρες όνομά τους, σε αυτήν την περίπτωση xmm0, αλλά ως m0, μια αφηρημένη μορφή (abstracted form). Σε μελλοντικά μαθήματα θα δείτε πώς αυτό σημαίνει ότι μπορείτε να γράψετε κώδικα μία φορά και να λειτουργεί σε πολλαπλά μεγέθη καταχωρητών SIMD. + +```assembly +paddb m0, m1 +``` + +Η paddb (διαβάστε το στο μυαλό σας ως *p-add-b*) προσθέτει κάθε byte σε κάθε καταχωρητή όπως φαίνεται παρακάτω. Το πρόθεμα «p» σημαίνει «packed» (συσκευασμένο) και χρησιμοποιείται για την αναγνώριση των διανυσματικών εντολών έναντι των βαθμωτών εντολών. Το επίθημα «b» δείχνει ότι πρόκειται για πρόσθεση ανά byte (πρόσθεση των bytes). + +| a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +\+ + +| q | r | s | t | u | v | w | x | y | z | aa | ab | ac | ad | ae | af | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +\= + +| a+q | b+r | c+s | d+t | e+u | f+v | g+w | h+x | i+y | j+z | k+aa | l+ab | m+ac | n+ad | o+ae | p+af | +| :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | :---- | + +```assembly +movu [srcq], m0 +``` + +Αυτό είναι γνωστό ως αποθήκευση (store). Τα δεδομένα εγγράφονται πίσω στη διεύθυνση του δείκτη srcq. + +```assembly +RET +``` + +Αυτή είναι μια μακροεντολή (macro) για να δηλώσει την επιστροφή της συνάρτησης. Σχεδόν όλες οι συναρτήσεις assembly στο FFmpeg τροποποιούν τα δεδομένα στα ορίσματα αντί να επιστρέφουν μια τιμή. + +Όπως θα δείτε στην εργασία, δημιουργούμε δείκτες συνάρτησης (function pointers) σε συναρτήσεις assembly και τους χρησιμοποιούμε όπου είναι διαθέσιμοι. + +[Επόμενο Μάθημα](../lesson_02/index.el.md) \ No newline at end of file diff --git a/lesson_02/index.el.md b/lesson_02/index.el.md new file mode 100644 index 0000000..b54b36e --- /dev/null +++ b/lesson_02/index.el.md @@ -0,0 +1,168 @@ +**Μάθημα Δεύτερο Γλώσσας Assembly του FFmpeg** + +Τώρα που γράψατε την πρώτη σας συνάρτηση σε γλώσσα assembly, θα εισαγάγουμε τις διακλαδώσεις (branches) και τους βρόχους (loops). + +Πρέπει πρώτα να εισαγάγουμε την ιδέα των ετικετών (labels) και των αλμάτων (jumps). Στο τεχνητό παράδειγμα παρακάτω, η εντολή jmp μετακινεί την εκτέλεση του κώδικα μετά το «.loop:». Το «.loop:» είναι γνωστό ως *ετικέτα* (label), με το πρόθεμα της τελείας να σημαίνει ότι είναι μια *τοπική ετικέτα* (local label), επιτρέποντάς σας ουσιαστικά να επαναχρησιμοποιήσετε το ίδιο όνομα ετικέτας σε πολλαπλές συναρτήσεις. Αυτό το παράδειγμα, φυσικά, δείχνει έναν ατέρμονα βρόχο, αλλά θα το επεκτείνουμε αργότερα σε κάτι πιο ρεαλιστικό. + +```assembly +mov r0q, 3 +.loop: + dec r0q + jmp .loop +``` + +Πριν δημιουργήσουμε έναν ρεαλιστικό βρόχο, πρέπει να εισαγάγουμε τον καταχωρητή *FLAGS*. Δεν θα σταθούμε πολύ στις περιπλοκές του *FLAGS* (πάλι επειδή οι λειτουργίες GPR είναι σε μεγάλο βαθμό σκαλωσιές), αλλά υπάρχουν αρκετές σημαίες (flags) όπως η Zero-Flag (Σημαία Μηδενός), η Sign-Flag (Σημαία Προσήμου) και η Overflow-Flag (Σημαία Υπερχείλισης), οι οποίες ορίζονται με βάση το αποτέλεσμα των περισσότερων εντολών εκτός της mov σε βαθμωτά δεδομένα, όπως αριθμητικές πράξεις και μετατοπίσεις. + +Ακολουθεί ένα παράδειγμα όπου ο μετρητής του βρόχου μετρά αντίστροφα μέχρι το μηδέν και η jg (άλμα αν είναι μεγαλύτερο από το μηδέν) είναι η συνθήκη του βρόχου. Η dec r0q θέτει τις σημαίες (FLAGs) με βάση την τιμή του r0q μετά την εντολή και μπορείτε να κάνετε άλμα με βάση αυτές. + +```assembly +mov r0q, 3 +.loop: + ; κάνε κάτι + dec r0q + jg .loop ; άλμα αν είναι μεγαλύτερο από το μηδέν +``` + +Αυτό είναι ισοδύναμο με τον ακόλουθο κώδικα C: + +```c +int i = 3; +do +{ + // κάνε κάτι + i--; +} while(i > 0) +``` + +Αυτός ο κώδικας C είναι λίγο αφύσικος. Συνήθως ένας βρόχος σε C γράφεται ως εξής: + +```c +int i; +for(i = 0; i < 3; i++) { + // κάνε κάτι +} +``` + +Αυτό είναι περίπου ισοδύναμο με (δεν υπάρχει απλός τρόπος να αντιστοιχιστεί αυτός ο βρόχος ```for```): + +```assembly +xor r0q, r0q +.loop: + ; κάνε κάτι + inc r0q + cmp r0q, 3 + jl .loop ; άλμα αν (r0q - 3) < 0, δηλαδή (r0q < 3) +``` + +Υπάρχουν πολλά πράγματα που πρέπει να επισημανθούν σε αυτό το απόσπασμα. Το πρώτο είναι το ```xor r0q, r0q```, που είναι ένας συνηθισμένος τρόπος για να μηδενίσετε έναν καταχωρητή, ο οποίος σε ορισμένα συστήματα είναι ταχύτερος από το ```mov r0q, 0```, επειδή, με απλά λόγια, δεν πραγματοποιείται πραγματική φόρτωση (load). Μπορεί επίσης να χρησιμοποιηθεί σε καταχωρητές SIMD με ```pxor m0, m0``` για να μηδενίσει έναν ολόκληρο καταχωρητή. Το επόμενο πράγμα που πρέπει να σημειωθεί είναι η χρήση της cmp. Η cmp ουσιαστικά αφαιρεί τον δεύτερο καταχωρητή από τον πρώτο (χωρίς να αποθηκεύει την τιμή πουθενά) και θέτει τις FLAGS, αλλά σύμφωνα με το σχόλιο, μπορεί να διαβαστεί μαζί με το άλμα, (jl = άλμα αν είναι μικρότερο από το μηδέν) για να γίνει άλμα εάν ```r0q < 3```. + +Παρατηρήστε πώς υπάρχει μια επιπλέον εντολή (cmp) σε αυτό το απόσπασμα. Γενικά μιλώντας, λιγότερες εντολές σημαίνουν ταχύτερο κώδικα, γι' αυτό και προτιμάται το προηγούμενο απόσπασμα. Όπως θα δείτε σε μελλοντικά μαθήματα, υπάρχουν περισσότερα κόλπα που χρησιμοποιούνται για την αποφυγή αυτής της επιπλέον εντολής και για τον ορισμό των FLAGS από μια αριθμητική ή άλλη πράξη. Σημειώστε πώς δεν γράφουμε assembly για να ταιριάζει ακριβώς με τους βρόχους της C, γράφουμε βρόχους για να τους κάνουμε όσο το δυνατόν γρηγορότερους στην assembly. + +Ακολουθούν ορισμένα συνηθισμένα μνημονικά άλματος που θα καταλήξετε να χρησιμοποιείτε (οι *FLAGS* παρατίθενται για πληρότητα, αλλά δεν χρειάζεται να γνωρίζετε τις λεπτομέρειες για να γράψετε βρόχους): + +| Mnemonic | Description | FLAGS | +| :---- | :---- | :---- | +| JE/JZ | Άλμα αν Ίσο/Μηδέν | ZF = 1 | +| JNE/JNZ | Άλμα αν Όχι Ίσο/Όχι Μηδέν | ZF = 0 | +| JG/JNLE | Άλμα αν Μεγαλύτερο/Όχι Μικρότερο ή Ίσο (προσημασμένο) | ZF = 0 and SF = OF | +| JGE/JNL | Άλμα αν Μεγαλύτερο ή Ίσο/Όχι Μικρότερο (προσημασμένο) | SF = OF | +| JL/JNGE | Άλμα αν Μικρότερο/Όχι Μεγαλύτερο ή Ίσο (προσημασμένο) | SF ≠ OF | +| JLE/JNG | Άλμα αν Μικρότερο ή Ίσο/Όχι Μεγαλύτερο (προσημασμένο) | ZF = 1 or SF ≠ OF | + +**Σταθερές** + +Ας δούμε μερικά παραδείγματα που δείχνουν πώς να χρησιμοποιούμε σταθερές: + +```assembly +SECTION_RODATA + +constants_1: db 1,2,3,4 +constants_2: times 2 dw 4,3,2,1 +``` + +* SECTION_RODATA καθορίζει ότι πρόκειται για ένα τμήμα δεδομένων μόνο για ανάγνωση (read-only data section). (Αυτή είναι μια μακροεντολή επειδή διαφορετικές μορφές αρχείων εξόδου που χρησιμοποιούν τα λειτουργικά συστήματα το δηλώνουν διαφορετικά) +* constants_1: Η ετικέτα constants_1, ορίζεται ως ```db``` (declare byte - δήλωση byte) - δηλαδή ισοδύναμο με το uint8_t constants_1[4] = {1, 2, 3, 4}; +* constants_2: Αυτό χρησιμοποιεί τη μακροεντολή ```times 2``` για να επαναλάβει τις δηλωμένες λέξεις - δηλαδή ισοδύναμο με το uint16_t constants_2[8] = {4, 3, 2, 1, 4, 3, 2, 1}; + +Αυτές οι ετικέτες, τις οποίες ο assembler μετατρέπει σε μια διεύθυνση μνήμης, μπορούν στη συνέχεια να χρησιμοποιηθούν σε φορτώσεις (loads) (αλλά όχι σε αποθηκεύσεις (stores) καθώς είναι μόνο για ανάγνωση). Ορισμένες εντολές δέχονται μια διεύθυνση μνήμης ως τελεστέο, οπότε μπορούν να χρησιμοποιηθούν χωρίς ρητές φορτώσεις σε έναν καταχωρητή (υπάρχουν πλεονεκτήματα και μειονεκτήματα σε αυτό). + +**Μετατοπίσεις (Offsets)** + +Οι μετατοπίσεις είναι η απόσταση (σε bytes) μεταξύ διαδοχικών στοιχείων στη μνήμη. Η μετατόπιση καθορίζεται από το **μέγεθος κάθε στοιχείου** στη δομή δεδομένων. + +Τώρα που μπορούμε να γράφουμε βρόχους, ήρθε η ώρα να ανακτήσουμε δεδομένα. Αλλά υπάρχουν μερικές διαφορές σε σύγκριση με τη C. Ας δούμε τον ακόλουθο βρόχο σε C: + +```c +uint32_t data[3]; +int i; +for(i = 0; i < 3; i++) { + data[i]; +} +``` + +Η μετατόπιση των 4-byte μεταξύ των στοιχείων των δεδομένων προϋπολογίζεται από τον μεταγλωττιστή της C. Αλλά όταν γράφετε assembly χειροκίνητα, πρέπει να υπολογίσετε αυτές τις μετατοπίσεις μόνοι σας. + +Ας δούμε τη σύνταξη για τους υπολογισμούς διευθύνσεων μνήμης. Αυτό ισχύει για όλους τους τύπους διευθύνσεων μνήμης: + +```assembly +[base + scale*index + disp] +``` + +* base - Αυτός είναι ένας GPR (συνήθως ένας δείκτης από ένα όρισμα συνάρτησης C) +* scale - Αυτό μπορεί να είναι 1, 2, 4, 8. Το 1 είναι η προεπιλογή +* index - Αυτός είναι ένας GPR (συνήθως ένας μετρητής βρόχου) +* disp - Αυτός είναι ένας ακέραιος (έως 32-bit). Η μετατόπιση (Displacement) είναι μια απόκλιση (offset) μέσα στα δεδομένα + +Η x86asm παρέχει τη σταθερά mmsize, η οποία σας επιτρέπει να γνωρίζετε το μέγεθος του καταχωρητή SIMD με τον οποίο εργάζεστε. + +Ακολουθεί ένα απλό (και χωρίς νόημα) παράδειγμα για να απεικονίσει τη φόρτωση από προσαρμοσμένες μετατοπίσεις: + +```assembly +;static void simple_loop(const uint8_t *src) +INIT_XMM sse2 +cglobal simple_loop, 1, 2, 2, src + movq r1q, 3 +.loop: + movu m0, [srcq] + movu m1, [srcq+2*r1q+3+mmsize] + + ; κάνε κάποια πράγματα + + add srcq, mmsize +dec r1q +jg .loop + +RET +``` + +Σημειώστε πώς στο ```movu m1, [srcq+2*r1q+3+mmsize]``` ο assembler θα προϋπολογίσει τη σωστή σταθερά μετατόπισης (displacement constant) που θα χρησιμοποιηθεί. Στο επόμενο μάθημα θα σας δείξουμε ένα κόλπο για να αποφύγετε να κάνετε add και dec στον βρόχο, αντικαθιστώντας τα με ένα μόνο add. + +**LEA** + +Τώρα που καταλαβαίνετε τις μετατοπίσεις (offsets), μπορείτε να χρησιμοποιήσετε την εντολή lea (Load Effective Address - Φόρτωση Ενεργής Διεύθυνσης). Αυτό σας επιτρέπει να εκτελέσετε πολλαπλασιασμό και πρόσθεση με μία μόνο εντολή, κάτι που θα είναι ταχύτερο από τη χρήση πολλαπλών εντολών. Υπάρχουν, φυσικά, περιορισμοί σχετικά με το με τι μπορείτε να πολλαπλασιάσετε και να προσθέσετε, αλλά αυτό δεν εμποδίζει την lea από το να είναι μια ισχυρή εντολή. + +```assembly +lea r0q, [base + scale*index + disp] +``` + +Αντίθετα με το όνομά της, η LEA μπορεί να χρησιμοποιηθεί για κανονική αριθμητική καθώς και για υπολογισμούς διευθύνσεων. Μπορείτε να κάνετε κάτι τόσο περίπλοκο όσο: + +```assembly +lea r0q, [r1q + 8*r2q + 5] +``` + +Σημειώστε ότι αυτό δεν επηρεάζει τα περιεχόμενα των r1q και r2q. Επίσης, δεν επηρεάζει τις FLAGS (οπότε δεν μπορείτε να κάνετε άλμα με βάση το αποτέλεσμα). Η χρήση της LEA αποφεύγει όλες αυτές τις εντολές και τους προσωρινούς καταχωρητές (αυτός ο κώδικας δεν είναι ισοδύναμος επειδή η add αλλάζει τις *FLAGS*): + +```assembly +movq r0q, r1q +movq r3q, r2q +sal r3q, 3 ; αριθμητική ολίσθηση αριστερά 3 = * 8 +add r3q, 5 +add r0q, r3q +``` + +Θα δείτε την lea να χρησιμοποιείται συχνά για τη ρύθμιση διευθύνσεων πριν από βρόχους ή για την εκτέλεση υπολογισμών όπως ο παραπάνω. Σημειώστε φυσικά, ότι δεν μπορείτε να κάνετε όλους τους τύπους πολλαπλασιασμού και πρόσθεσης, αλλά οι πολλαπλασιασμοί με 1, 2, 4, 8 και η πρόσθεση μιας σταθερής μετατόπισης είναι συνηθισμένοι. + +Στην εργασία θα πρέπει να φορτώσετε μια σταθερά και να προσθέσετε τις τιμές σε ένα διάνυσμα SIMD μέσα σε έναν βρόχο. + +[Επόμενο Μάθημα](../lesson_03/index.el.md) \ No newline at end of file diff --git a/lesson_03/index.el.md b/lesson_03/index.el.md new file mode 100644 index 0000000..7f7981a --- /dev/null +++ b/lesson_03/index.el.md @@ -0,0 +1,202 @@ +**Μάθημα Τρίτο Γλώσσας Assembly του FFmpeg** + +Ας εξηγήσουμε λίγη ακόμα ορολογία και να σας κάνουμε ένα σύντομο μάθημα ιστορίας. + +**Σύνολα Εντολών (Instruction Sets)** + +Μπορεί να είδατε στο προηγούμενο μάθημα ότι μιλήσαμε για το SSE2, το οποίο είναι ένα σύνολο εντολών SIMD. Όταν κυκλοφορεί μια νέα γενιά CPU, μπορεί να συνοδεύεται από νέες εντολές και μερικές φορές μεγαλύτερα μεγέθη καταχωρητών. Η ιστορία του συνόλου εντολών x86 είναι πολύ περίπλοκη, οπότε αυτή είναι μια απλοποιημένη ιστορία (υπάρχουν πολλές περισσότερες υποκατηγορίες): + +* MMX - Κυκλοφόρησε το 1997, το πρώτο SIMD σε επεξεργαστές Intel, καταχωρητές 64-bit, ιστορικό +* SSE (Streaming SIMD Extensions) - Κυκλοφόρησε το 1999, καταχωρητές 128-bit +* SSE2 - Κυκλοφόρησε το 2000, πολλές νέες εντολές +* SSE3 - Κυκλοφόρησε το 2004, πρώτες οριζόντιες εντολές (horizontal instructions) +* SSSE3 (Supplemental SSE3) - Κυκλοφόρησε το 2006, νέες εντολές αλλά το πιο σημαντικό η εντολή αναδιάταξης (shuffle) pshufb, αναμφισβήτητα η πιο σημαντική εντολή στην επεξεργασία βίντεο +* SSE4 - Κυκλοφόρησε το 2008, πολλές νέες εντολές συμπεριλαμβανομένων των packed minimum και maximum. +* AVX - Κυκλοφόρησε το 2011, καταχωρητές 256-bit (μόνο για κινητή υποδιαστολή - float) και νέα σύνταξη τριών τελεστέων +* AVX2 - Κυκλοφόρησε το 2013, καταχωρητές 256-bit για εντολές ακεραίων +* AVX512 - Κυκλοφόρησε το 2017, καταχωρητές 512-bit, νέα δυνατότητα μάσκας λειτουργίας (operation mask). Αυτά είχαν περιορισμένη χρήση εκείνη την εποχή στο FFmpeg λόγω της μείωσης της συχνότητας της CPU (downscaling) όταν χρησιμοποιούνταν νέες εντολές. Πλήρης αναδιάταξη (shuffle/permute) 512-bit με την vpermb. +* AVX512ICL - Κυκλοφόρησε το 2019, όχι πλέον μείωση της συχνότητας του ρολογιού. +* AVX10 - Προσεχώς + +Αξίζει να σημειωθεί ότι τα σύνολα εντολών μπορούν να αφαιρεθούν καθώς και να προστεθούν στους επεξεργαστές (CPU). Για παράδειγμα, το AVX512 [αφαιρέθηκε](https://www.igorslab.de/en/intel-deactivated-avx-512-on-alder-lake-but-fully-questionable-interpretation-of-efficiency-news-editorial/), με αμφιλεγόμενο τρόπο, στους επεξεργαστές Intel 12ης Γενιάς. Γι' αυτόν τον λόγο, το FFmpeg κάνει ανίχνευση CPU κατά το χρόνο εκτέλεσης (runtime). Το FFmpeg ανιχνεύει τις δυνατότητες του επεξεργαστή στον οποίο εκτελείται. + +Όπως είδατε στην εργασία, οι δείκτες συναρτήσεων (function pointers) είναι από προεπιλογή C και αντικαθίστανται με μια συγκεκριμένη παραλλαγή συνόλου εντολών. Αυτό σημαίνει ότι η ανίχνευση γίνεται μία φορά και δεν χρειάζεται να ξαναγίνει ποτέ. Αυτό έρχεται σε αντίθεση με πολλές ιδιόκτητες εφαρμογές οι οποίες κωδικοποιούν ένα συγκεκριμένο σύνολο εντολών, καθιστώντας έναν απόλυτα λειτουργικό υπολογιστή παρωχημένο. Αυτό επιτρέπει επίσης την ενεργοποίηση/απενεργοποίηση βελτιστοποιημένων συναρτήσεων κατά το χρόνο εκτέλεσης. Αυτό είναι ένα από τα μεγάλα οφέλη του ανοιχτού κώδικα. + +Προγράμματα όπως το FFmpeg χρησιμοποιούνται σε δισεκατομμύρια συσκευές σε όλο τον κόσμο, μερικές από τις οποίες μπορεί να είναι πολύ παλιές. Το FFmpeg τεχνικά υποστηρίζει μηχανήματα που υποστηρίζουν μόνο SSE, τα οποία είναι 25 ετών! Ευτυχώς, το x86inc.asm είναι σε θέση να σας πει εάν χρησιμοποιείτε μια εντολή που δεν είναι διαθέσιμη σε ένα συγκεκριμένο σύνολο εντολών. + +Για να σας δώσουμε μια ιδέα για τις πραγματικές δυνατότητες, ακολουθεί η διαθεσιμότητα του συνόλου εντολών από την [Έρευνα του Steam](https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam) από τον Νοέμβριο του 2024 (αυτό είναι προφανώς μεροληπτικό προς τους παίκτες): + +| Σύνολο Εντολών | Διαθεσιμότητα | +| :---- | :---- | +| SSE2 | 100% | +| SSE3 | 100% | +| SSSE3 | 99.86% | +| SSE4.1 | 99.80% | +| AVX | 97.39% | +| AVX2 | 94.44% | +| AVX512 (Το Steam δεν διαχωρίζει μεταξύ AVX512 και AVX512ICL) | 14.09% | + +Για μια εφαρμογή όπως το FFmpeg με δισεκατομμύρια χρήστες, ακόμη και το 0,1% είναι ένας πολύ μεγάλος αριθμός χρηστών και αναφορών σφαλμάτων εάν κάτι πάει στραβά. Το FFmpeg διαθέτει εκτεταμένη υποδομή δοκιμών για τον έλεγχο των παραλλαγών CPU/ΛΣ/Μεταγλωττιστή στη [σουίτα δοκιμών FATE](https://fate.ffmpeg.org/?query=subarch:x86_64%2F%2F). Κάθε commit εκτελείται σε εκατοντάδες μηχανήματα για να διασφαλιστεί ότι τίποτα δεν χαλάει. + +Η Intel παρέχει ένα λεπτομερές εγχειρίδιο συνόλου εντολών εδώ: +[https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) + +Μπορεί να είναι δυσκίνητο να ψάχνετε σε ένα PDF, οπότε υπάρχει μια ανεπίσημη εναλλακτική λύση που βασίζεται στο διαδίκτυο εδώ: [https://www.felixcloutier.com/x86/](https://www.felixcloutier.com/x86/) + +Υπάρχει επίσης μια οπτική αναπαράσταση των εντολών SIMD διαθέσιμη εδώ: [https://www.officedaytime.com/simd512e/](https://www.officedaytime.com/simd512e/) + +Μέρος της πρόκλησης της assembly x86 είναι η εύρεση της σωστής εντολής για τις ανάγκες σας. Σε ορισμένες περιπτώσεις, οι εντολές μπορούν να χρησιμοποιηθούν με τρόπο που δεν προορίζονταν αρχικά. + +**Τεχνάσματα με τις μετατοπίσεις δεικτών (Pointer offset trickery)** + +Ας επιστρέψουμε στην αρχική μας συνάρτηση από το Μάθημα 1, αλλά ας προσθέσουμε ένα όρισμα width (πλάτος) στη συνάρτηση C. + +Χρησιμοποιούμε ptrdiff_t για τη μεταβλητή width αντί για int για να διασφαλίσουμε ότι τα ανώτερα 32-bit του 64-bit ορίσματος είναι μηδέν. Αν περνούσαμε απευθείας ένα int width στην υπογραφή της συνάρτησης και στη συνέχεια προσπαθούσαμε να το χρησιμοποιήσουμε ως τετραπλή λέξη (quadword) για αριθμητική δεικτών (δηλαδή χρησιμοποιώντας το `widthq`), τα ανώτερα 32-bit του καταχωρητή θα μπορούσαν να γεμίσουν με αυθαίρετες τιμές. Θα μπορούσαμε να το διορθώσουμε αυτό κάνοντας επέκταση προσήμου (sign extending) στο width με την εντολή `movsxd` (δείτε επίσης τη μακροεντολή `movsxdifnidn` στο x86inc.asm), αλλά αυτός είναι ένας ευκολότερος τρόπος. + +Η παρακάτω συνάρτηση περιέχει το τέχνασμα με τη μετατόπιση του δείκτη: + +```assembly +;static void add_values(uint8_t *src, const uint8_t *src2, ptrdiff_t width) +INIT_XMM sse2 +cglobal add_values, 3, 3, 2, src, src2, width + add srcq, widthq + add src2q, widthq + neg widthq + +.loop + movu m0, [srcq+widthq] + movu m1, [src2q+widthq] + + paddb m0, m1 + + movu [srcq+widthq], m0 + add widthq, mmsize + jl .loop + + RET +``` + +Ας το δούμε βήμα προς βήμα καθώς μπορεί να προκαλέσει σύγχυση: + +```assembly + add srcq, widthq + add src2q, widthq + neg widthq +``` + +Το πλάτος (width) προστίθεται σε κάθε δείκτη έτσι ώστε κάθε δείκτης να δείχνει πλέον στο τέλος του buffer που πρόκειται να επεξεργαστεί. Στη συνέχεια, το πλάτος αναιρείται (negated). + +```assembly + movu m0, [srcq+widthq] + movu m1, [src2q+widthq] +``` + +Οι φορτώσεις (loads) γίνονται στη συνέχεια με το widthq να είναι αρνητικό. Έτσι, στην πρώτη επανάληψη, το [srcq+widthq] δείχνει στην αρχική διεύθυνση του srcq, δηλαδή δείχνει πίσω στην αρχή του buffer. + +```assembly + add widthq, mmsize + jl .loop +``` + +Το mmsize προστίθεται στο αρνητικό widthq φέρνοντάς το πιο κοντά στο μηδέν. Η συνθήκη του βρόχου είναι τώρα jl (άλμα αν είναι μικρότερο από το μηδέν). Αυτό το τέχνασμα σημαίνει ότι το widthq χρησιμοποιείται ως μετατόπιση δείκτη **και** ως μετρητής βρόχου ταυτόχρονα, εξοικονομώντας μια εντολή cmp. Επιτρέπει επίσης τη χρήση της μετατόπισης του δείκτη σε πολλαπλές φορτώσεις και αποθηκεύσεις, καθώς και τη χρήση πολλαπλασίων των μετατοπίσεων του δείκτη εάν χρειαστεί (να το θυμάστε αυτό για την εργασία). + +**Ευθυγράμμιση (Alignment)** + +Σε όλα τα παραδείγματά μας χρησιμοποιούσαμε την εντολή movu για να αποφύγουμε το θέμα της ευθυγράμμισης. Πολλοί επεξεργαστές (CPU) μπορούν να φορτώσουν και να αποθηκεύσουν δεδομένα ταχύτερα εάν τα δεδομένα είναι ευθυγραμμισμένα, δηλαδή εάν η διεύθυνση μνήμης είναι διαιρετή με το μέγεθος του καταχωρητή SIMD. Όπου είναι δυνατόν, προσπαθούμε να χρησιμοποιούμε ευθυγραμμισμένες φορτώσεις και αποθηκεύσεις στο FFmpeg χρησιμοποιώντας την εντολή mova. + +Στο FFmpeg, η av_malloc είναι σε θέση να παρέχει ευθυγραμμισμένη μνήμη στο σωρό (heap) και η οδηγία προεπεξεργαστή C DECLARE_ALIGNED μπορεί να παρέχει ευθυγραμμισμένη μνήμη στη στοίβα (stack). Εάν η mova χρησιμοποιηθεί με μια μη ευθυγραμμισμένη διεύθυνση, θα προκαλέσει σφάλμα κατάτμησης (segmentation fault) και η εφαρμογή θα καταρρεύσει. Είναι επίσης σημαντικό να είστε βέβαιοι ότι η τιμή ευθυγράμμισης αντιστοιχεί στο μέγεθος του καταχωρητή SIMD, δηλαδή 16 με xmm, 32 για ymm και 64 για zmm. + +Δείτε πώς μπορείτε να ευθυγραμμίσετε την αρχή του τμήματος RODATA σε 64-byte: + +```assembly +SECTION_RODATA 64 +``` + +Σημειώστε ότι αυτό ευθυγραμμίζει απλώς την αρχή του RODATA. Μπορεί να χρειαστούν bytes συμπλήρωσης (padding bytes) για να διασφαλιστεί ότι η επόμενη ετικέτα παραμένει σε ένα όριο 64-byte. + +**Επέκταση εύρους (Range expansion)** + +Ένα άλλο θέμα που έχουμε αποφύγει μέχρι τώρα είναι η υπερχείλιση (overflowing). Αυτό συμβαίνει, για παράδειγμα, όταν η τιμή ενός byte ξεπερνά το 255 μετά από μια πράξη όπως η πρόσθεση ή ο πολλαπλασιασμός. Μπορεί να θέλουμε να εκτελέσουμε μια πράξη όπου χρειαζόμαστε μια ενδιάμεση τιμή μεγαλύτερη από ένα byte (π.χ. λέξεις - words), ή ενδεχομένως να θέλουμε να αφήσουμε τα δεδομένα σε αυτό το μεγαλύτερο ενδιάμεσο μέγεθος. + +Για τα μη προσημασμένα bytes (unsigned bytes), εδώ είναι που μπαίνουν στο παιχνίδι οι εντολές punpcklbw (packed unpack low bytes to words - συσκευασμένη αποσυσκευασία χαμηλών bytes σε λέξεις) και punpckhbw (packed unpack high bytes to words - συσκευασμένη αποσυσκευασία υψηλών bytes σε λέξεις). + +Ας δούμε πώς λειτουργεί η punpcklbw. Η σύνταξη για την έκδοση SSE2 από το Εγχειρίδιο της Intel είναι η εξής: + +| PUNPCKLBW xmm1, xmm2/m128 | +| :---- | + +Αυτό σημαίνει ότι η πηγή της (δεξιά πλευρά) μπορεί να είναι ένας καταχωρητής xmm ή μια διεύθυνση μνήμης (m128 σημαίνει μια διεύθυνση μνήμης με την τυπική σύνταξη [base + scale*index + disp]) και ο προορισμός ένας καταχωρητής xmm. + +Η ιστοσελίδα officedaytime.com που αναφέρθηκε παραπάνω έχει ένα καλό διάγραμμα που δείχνει τι συμβαίνει: + +![What is this](image1.png) + +Μπορείτε να δείτε ότι τα bytes είναι πλεγμένα (interleaved) από το κάτω μισό του κάθε καταχωρητή αντίστοιχα. Αλλά τι σχέση έχει αυτό με την επέκταση εύρους; Αν ο καταχωρητής πηγής (src) είναι εξ ολοκλήρου μηδενικά, αυτό πλέκει τα bytes του καταχωρητή προορισμού (dst) με μηδενικά. Αυτό είναι γνωστό ως *επέκταση με μηδενικά* (zero extension), καθώς τα bytes είναι χωρίς πρόσημο (unsigned). Η punpckhbw μπορεί να χρησιμοποιηθεί για να κάνει το ίδιο πράγμα για τα υψηλά bytes (high bytes). + +Ακολουθεί ένα απόσπασμα κώδικα που δείχνει πώς γίνεται αυτό: + +```assembly +pxor m2, m2 ; μηδενισμός του m2 + +movu m0, [srcq] +movu m1, m0 ; δημιουργία αντιγράφου του m0 στο m1 +punpcklbw m0, m2 +punpckhbw m1, m2 +``` + +Οι καταχωρητές ```m0``` και ```m1``` περιέχουν τώρα τα αρχικά bytes επεκταμένα με μηδενικά σε λέξεις (words). Στο επόμενο μάθημα θα δείτε πώς οι εντολές τριών τελεστέων (three-operand instructions) στο AVX καθιστούν περιττή τη δεύτερη movu. + +**Επέκταση προσήμου (Sign extension)** + +Τα προσημασμένα δεδομένα (signed data) είναι λίγο πιο περίπλοκα. Για να επεκτείνουμε το εύρος ενός προσημασμένου ακεραίου, πρέπει να χρησιμοποιήσουμε μια διαδικασία γνωστή ως [επέκταση προσήμου](https://en.wikipedia.org/wiki/Sign_extension). Αυτή συμπληρώνει τα πιο σημαντικά bits (MSBs) με το bit προσήμου. Για παράδειγμα: το -2 σε int8_t είναι 0b11111110. Για να το επεκτείνουμε με πρόσημο σε int16_t, το MSB του 1 επαναλαμβάνεται για να γίνει 0b1111111111111110. + +Η εντολή ```pcmpgtb``` (packed compare greater than byte) μπορεί να χρησιμοποιηθεί για επέκταση προσήμου. Κάνοντας τη σύγκριση (0 > byte), όλα τα bits στο byte προορισμού τίθενται σε 1 εάν το byte είναι αρνητικό, διαφορετικά τα bits στο byte προορισμού τίθενται σε 0. Η punpckX μπορεί να χρησιμοποιηθεί όπως παραπάνω για την εκτέλεση της επέκτασης προσήμου. Εάν το byte είναι αρνητικό, το αντίστοιχο byte είναι 0b11111111 και διαφορετικά είναι 0x00000000. Η εναλλαγή (interleaving) της τιμής του byte με την έξοδο της pcmpgtb εκτελεί ως αποτέλεσμα μια επέκταση προσήμου σε λέξη (word). + +```assembly +pxor m2, m2 ; μηδενισμός του m2 + +movu m0, [srcq] +movu m1, m0 ; δημιουργία αντιγράφου του m0 στο m1 + +pcmpgtb m2, m0 +punpcklbw m0, m2 +punpckhbw m1, m2 +``` + +Όπως μπορείτε να δείτε, υπάρχει μια επιπλέον εντολή σε σύγκριση με την περίπτωση των μη προσημασμένων (unsigned). + +**Συσκευασία (Packing)** + +Οι εντολές packuswb (pack unsigned word to byte - συσκευασία μη προσημασμένης λέξης σε byte) και packsswb σας επιτρέπουν να μεταβείτε από λέξη (word) σε byte. Σας επιτρέπουν να πλέξετε (interleave) δύο καταχωρητές SIMD που περιέχουν λέξεις σε έναν καταχωρητή SIMD με bytes. Σημειώστε ότι εάν οι τιμές υπερβούν το εύρος του byte, θα κορεστούν (δηλαδή θα περιοριστούν στη μεγαλύτερη τιμή - clamped at the largest value). + +**Αναδιατάξεις (Shuffles)** + +Οι αναδιατάξεις, επίσης γνωστές ως μεταθέσεις (permutes), είναι αναμφισβήτητα η πιο σημαντική εντολή στην επεξεργασία βίντεο και η pshufb (packed shuffle bytes - συσκευασμένη αναδιάταξη bytes), διαθέσιμη στο SSSE3, είναι η πιο σημαντική παραλλαγή. + +Για κάθε byte, το αντίστοιχο byte προέλευσης χρησιμοποιείται ως δείκτης του καταχωρητή προορισμού, εκτός όταν το MSB (πιο σημαντικό bit) είναι ενεργοποιημένο, οπότε το byte προορισμού μηδενίζεται. Είναι ανάλογο με τον ακόλουθο κώδικα C (αν και στο SIMD όλες οι 16 επαναλήψεις του βρόχου συμβαίνουν παράλληλα): + +```c +for(int i = 0; i < 16; i++) { + if(src[i] & 0x80) + dst[i] = 0; + else + dst[i] = dst[src[i]] +} +``` +Ακολουθεί ένα απλό παράδειγμα assembly: + +```assembly +SECTION_DATA 64 + +shuffle_mask: db 4, 3, 1, 2, -1, 2, 3, 7, 5, 4, 3, 8, 12, 13, 15, -1 + +section .text + +movu m0, [srcq] +movu m1, [shuffle_mask] +pshufb m0, m1 ; αναδιάταξη του m0 με βάση το m1 +``` + +Σημειώστε ότι το -1 για ευκολία στην ανάγνωση χρησιμοποιείται ως δείκτης αναδιάταξης για να μηδενίσει το byte εξόδου: το -1 ως byte είναι το πεδίο bit 0b11111111 (συμπλήρωμα ως προς δύο), και επομένως το MSB (0x80) είναι ενεργοποιημένο. + +[image1]: \ No newline at end of file