Κάθε πράξη SIMD μέχρι τώρα δρούσε λωρίδα προς λωρίδα.
Η τιμή στη λωρίδα i του αποτελέσματος υπολογίζεται με βάση τη λωρίδα i των εισόδων.
Ωστόσο, υπάρχουν πολλές περιπτώσεις όπου η μετακίνηση τιμών ανάμεσα σε λωρίδες είναι απαραίτητη ή επιθυμητή. Για παράδειγμα, οι λωρίδες μπορεί να μην είναι στη σωστή σειρά για τον υπολογισμό που θέλουμε να κάνουμε.
Υπάρχουν πολλές εντολές SIMD που μετακινούν δεδομένα ανάμεσα σε λωρίδες με διαφορετικούς τρόπους.
Μια αναδιάταξη αλλάζει τη σειρά των byte ή των λωρίδων ενός καταχωρητή, αντλώντας κάθε λωρίδα προορισμού από μια λωρίδα πηγής που μπορεί να βρίσκεται οπουδήποτε. Μπορούν να επιλέξουν την ίδια λωρίδα για διαφορετικούς προορισμούς, κι έτσι είναι σε θέση να μεταδίδουν και τιμές.
Οι εντολές αναδιάταξης συμπεριφέρονται διαφορετικά ανάλογα με το μέγεθος και το πεδίο εκτέλεσής τους.
Η εντολή pshufd αναδιατάσσει τις τέσσερις λωρίδες των 32 bit της πηγής της στον προορισμό της.
Η επιλογή είναι μια άμεση τιμή 8 bit, που διαβάζεται ως τέσσερα πεδία των 2 bit, ένα για κάθε λωρίδα προορισμού. Κάθε πεδίο επιλέγει ποια από τις τέσσερις λωρίδες πηγής θα αντιγραφεί σε εκείνη τη λωρίδα προορισμού:
| field | lane index |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
Η θέση του πεδίου μέσα στην άμεση τιμή, διαβασμένη από δεξιά προς τα αριστερά, δείχνει πού εισάγεται η επιλεγμένη λωρίδα. Μια λωρίδα πηγής μπορεί να επιλεγεί περισσότερες από μία φορές, κι έτσι μια μόνο λωρίδα μεταδίδεται σε ολόκληρο τον καταχωρητή:
; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
Υπάρχουν δύο εντολές αναδιάταξης κινητής υποδιαστολής, που ακολουθούν την ίδια γενική σύνταξη: shuf + p + την κατάληξη μεγέθους (είτε s είτε d).
Χρησιμοποιούν επίσης μια άμεση τιμή για να επιλέξουν τις λωρίδες.
Το shufps χρησιμοποιεί την ίδια κωδικοποίηση πεδίων των 2 bit με τις προηγούμενες ακέραιες αναδιατάξεις.
Ωστόσο, επειδή υπάρχουν μόνο 2 λωρίδες των 64 bit σε έναν τελεστέο 128 bit, το shufpd χρησιμοποιεί κωδικοποίηση πεδίου μόνο 1 bit.
Αυτές οι εντολές διαφέρουν από τις ακέραιες ομόλογές τους στο ότι αντλούν τις λωρίδες από δύο τελεστέους αντί για έναν:
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1 ; xmm4 = {xmm4[1], xmm5[0]}
Επειδή και οι δύο τελεστέοι τροφοδοτούν το αποτέλεσμα, μπορούν να χρησιμοποιηθούν για να διαπλέξουν δύο διανύσματα σε ένα βήμα. Αν πηγή και προορισμός είναι ο ίδιος καταχωρητής, κάθε λωρίδα αντλείται από αυτόν.
Το pshufb είναι η πιο γενική αναδιάταξη.
Παρόλο που τα pshufb και pshufd έχουν πολύ παρόμοια ονόματα, με διαφορά μόνο στην κατάληξη μεγέθους, εκτελούν εντελώς διαφορετικές πράξεις.
Πρώτον, ενώ το pshufd χρησιμοποιεί μια άμεση τιμή για να επιλέξει θέσεις λωρίδων, το pshufb χρησιμοποιεί ένα διάνυσμα ελέγχου στον τελεστέο πηγής.
Αυτό το διάνυσμα ελέγχου είναι ένας καταχωρητής xmm ή ένας τελεστέος μνήμης 16 byte.
Για καθεμία από τις 16 λωρίδες προορισμού, τα χαμηλά τέσσερα bit του διανύσματος ελέγχου δίνουν τη θέση ενός byte πηγής, από 0 έως 15.
Αν η λωρίδα i του διανύσματος ελέγχου περιέχει τη θέση byte πηγής j, τότε η λωρίδα j-th του προορισμού θα μετακινηθεί στη λωρίδα i.
Αυτό αναδεικνύει μια δεύτερη διαφορά ανάμεσα στις δύο εντολές.
Ενώ το pshufd παίρνει τις λωρίδες προς αναδιάταξη από έναν διαφορετικό τελεστέο πηγής, το pshufb εκτελεί την αναδιάταξη επιτόπου στον προορισμό.
section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
section .text
fn:
pshufb xmm0, [rel reverse]
; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
; ...
; in the end, the bytes in xmm0 are reversed
Το pshufb είναι ακόμη πιο ευέλικτο από αυτό: μπορεί να μηδενίσει οποιαδήποτε από τις λωρίδες.
Αν το ανώτερο bit είναι ενεργό σε μια λωρίδα i του διανύσματος ελέγχου, τότε η λωρίδα i-th του προορισμού μηδενίζεται.
Έτσι, το pshufb είναι ταυτόχρονα μια αυθαίρετη μετάθεση byte και ένας επιλεκτικός μηδενισμός, μέσα σε μία μόνο εντολή.
Επειδή το pshufb δρα σε επίπεδο byte, μπορεί επίσης να χρησιμοποιηθεί για την αναδιάταξη λωρίδων διαφορετικών μεγεθών, ομαδοποιώντας γειτονικές λωρίδες.
Για παράδειγμα, μπορεί να χρησιμοποιηθεί για την αναδιάταξη dword:
section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes
section .text
fn:
movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
pshufb xmm0, [rel reverse_dwords]
; xmm0 = {49, 37, 25, 13}
Οι εντολές unpack παίρνουν λωρίδες από δύο τελεστέους και τις πλέκουν μεταξύ τους, εναλλάσσοντας ανάμεσα στους δύο. Υπάρχουν παραλλαγές για ακέραιους και για κινητή υποδιαστολή και ακολουθούν σε μεγάλο βαθμό την ίδια γενική δομή σύνταξης, με δύο διαφορές:
l ή h που δείχνει αν δρα στο χαμηλό μισό (l) ή στο υψηλό μισό (h) κάθε τελεστέου.bw ή qdq (η κατάληξη μεγέθους για 16 byte είναι dq, όπως στο movdqu).punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}
unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}
Οι εντολές pack κινούνται προς την αντίθετη κατεύθυνση, συνδυάζοντας τις λωρίδες δύο τελεστέων σε λωρίδες μισού πλάτους στον προορισμό.
Αυτές οι εντολές δεν παίρνουν πρόθεμα p.
Κατά τα άλλα, η σύνταξη συνδυάζει στοιχεία που έχουμε ήδη δει:
pack.s ή u που δείχνει αν οι τιμές εξόδου είναι με πρόσημο ή χωρίς πρόσημο, αντίστοιχα.s για κορεσμό, όπως στην αριθμητική με κορεσμό που είδαμε σε προηγούμενη έννοια.unpck).Επειδή η πράξη pack είναι στενωτική, δεν χρειάζεται κατάληξη l ή h:
packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)
Αυτές οι εντολές εφαρμόζουν κορεσμό, δηλαδή οι τιμές εξόδου περιορίζονται στο στενότερο εύρος. Λειτουργούν από dword σε word και από word σε byte. Δεν υπάρχει παραλλαγή από qword σε dword.
Σημείωσε ότι η είσοδος θεωρείται πάντα ότι έχει πρόσημο.
Ο τύπος, με πρόσημο ή χωρίς πρόσημο, αφορά την έξοδο.
Δείχνει το εύρος στο οποίο θα γίνει ο περιορισμός.
Για παράδειγμα, το packsswb περιορίζει στο εύρος ενός byte με πρόσημο, δηλαδή [-128, 127].
Οι χαμηλές λωρίδες του αποτελέσματος προέρχονται από τον τελεστέο προορισμού και οι υψηλές από την πηγή:
packusdw xmm0, xmm1 ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}
Αυτές οι εντολές δεν έχουν αντίστοιχη έκδοση για κινητή υποδιαστολή.
Μέχρι τώρα, μετακινούσαμε πάντα δεδομένα ανάμεσα σε έναν καταχωρητή SIMD και έναν καταχωρητή γενικού σκοπού χρησιμοποιώντας τα movq/movd.
Αυτές οι εντολές μπορούν να γράψουν ή να διαβάσουν μόνο τη χαμηλή λωρίδα ενός καταχωρητή SIMD, και, όταν γράφουν, μηδενίζουν όλες τις άλλες λωρίδες.
Υπάρχουν εντολές που κάνουν το ίδιο για οποιαδήποτε λωρίδα, όχι μόνο την πρώτη, αφήνοντας τις άλλες λωρίδες άθικτες:
Και οι δύο ακολουθούν τη σύνταξη των ακεραίων: p + insr/extr + την κατάληξη μεγέθους (b, w, d ή q).
Και στις δύο περιπτώσεις, ο καταχωρητής γενικού σκοπού είναι συνήθως πλάτους 32 bit.
Μόνο τα pinsrq και pextrq χρειάζονται τελεστέο 64 bit.
Ένας τελεστέος μνήμης έχει πάντα το μέγεθος της πράξης: 8 bit για τα pinsrb/pextrb, 16 bit για τα pinsrw/pextrw, και ούτω καθεξής.
pinsrb xmm0, eax, 5 ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx
Σημείωσε ότι αυτές οι εντολές, όπως ακριβώς τα movd και movq, απλώς μετακινούν ακατέργαστα byte.
Αυτό σημαίνει ότι μπορούν να χρησιμοποιηθούν και για τιμές κινητής υποδιαστολής που είναι αποθηκευμένες στη μνήμη ή σε έναν καταχωρητή γενικού σκοπού.
Γράφεις τους εσωτερικούς βρόχους ενός pipeline λογισμικού για εικόνες, του σταδίου που προετοιμάζει τις υφές και συνθέτει τα επίπεδα πριν φτάσουν στην οθόνη. Το pipeline δουλεύει πάνω σε πίξελ ένα μπλοκ τη φορά, εφαρμόζοντας την ίδια πράξη σε ολόκληρο το μπλοκ.
Ένα πίξελ αποτελείται από τέσσερα κανάλια του 1 byte: κόκκινο, πράσινο, μπλε και άλφα, με αυτή τη σειρά (RGBA).
Ένα μπλοκ είναι 4 πίξελ, δηλαδή 16 byte συνολικά.
Έχεις πέντε εργασίες.
Δέχεσαι τους τελεστέους μέσω διευθύνσεων μνήμης και γράφεις την απάντησή σου μέσω μιας διεύθυνσης αποτελέσματος. Όλες οι διευθύνσεις μνήμης σε αυτή την άσκηση είναι ευθυγραμμισμένες στα 16 byte.
Οι υπολογισμοί σε αυτή την άσκηση πρέπει να εκτελούνται χρησιμοποιώντας εντολές SIMD, όχι βαθμωτές πράξεις.
Οι υφές αποθηκεύονται ως RGBA, αλλά το framebuffer στο οποίο σχεδιάζει αυτό το pipeline περιμένει κάθε πίξελ με σειρά BGRA: τα κανάλια κόκκινου και μπλε αντεστραμμένα, τα κανάλια πράσινου και άλφα στη θέση τους.
Μια εικόνα φτάνει ως ακολουθία μπλοκ και κάθε μπλοκ μετατρέπεται με τον ίδιο τρόπο.
Υλοποίησε τη συνάρτηση to_display_order, η οποία μετατρέπει μια ολόκληρη εικόνα από RGBA σε BGRA, ένα μπλοκ τη φορά.
Θα πρέπει να ορίσεις τη μάσκα ελέγχου για την αναδιάταξη των καναλιών ως πακεταρισμένη σταθερά στη μνήμη και να την ξαναχρησιμοποιήσεις για κάθε μπλοκ.
Αυτή η συνάρτηση δέχεται ως ορίσματα, με αυτή τη σειρά:
result: διεύθυνση μνήμης για ένα buffer όπου γράφονται τα μετατρεπόμενα μπλοκ, 16 byte ανά μπλοκ.pixels: διεύθυνση μνήμης των μπλοκ προέλευσης, 4 πίξελ ανά μπλοκ, κάθε πίξελ 4 byte με σειρά RGBA.block_count: ο αριθμός των μπλοκ, πάντα μεγαλύτερος από 0.pixels = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}
Αυτή η συνάρτηση δεν έχει τιμή επιστροφής.
Για να καθαρίσει μια περιοχή ή να βάψει ένα ομοιόμορφο τμήμα, το pipeline γράφει ένα μόνο χρώμα σε κάθε πίξελ της περιοχής.
Υλοποίησε τη συνάρτηση fill_region, η οποία γεμίζει μια περιοχή block_count μπλοκ με αντίγραφα ενός χρώματος.
Αυτή η συνάρτηση δέχεται ως ορίσματα, με αυτή τη σειρά:
result: διεύθυνση μνήμης για ένα buffer όπου γράφονται τα γεμάτα μπλοκ, 16 byte ανά μπλοκ.color: διεύθυνση μνήμης ενός πίξελ, 4 byte με σειρά RGBA.block_count: ο αριθμός των μπλοκ που θα γεμίσουν, πάντα μεγαλύτερος από 0.color = {18, 52, 86, 120}
block_count = 2
result = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}
Αυτή η συνάρτηση δεν έχει τιμή επιστροφής.
Δύο επίπεδα με ένα κανάλι πρέπει να συγχωνευτούν σε ένα buffer, με τα δείγματά τους διαπλεκόμενα. Το αποτέλεσμα εναλλάσσει ένα δείγμα από το πρώτο επίπεδο και μετά ένα από το δεύτερο.
Υλοποίησε τη συνάρτηση weave_scanlines, η οποία διαπλέκει δύο σειρές των 16 δειγμάτων η καθεμία σε μία μόνο σειρά 32 δειγμάτων.
Αυτή η συνάρτηση δέχεται ως ορίσματα, με αυτή τη σειρά:
result: διεύθυνση μνήμης για ένα buffer όπου γράφονται τα 32 διαπλεκόμενα δείγματα.first: διεύθυνση μνήμης της πρώτης σειράς, 16 δείγματα, καθένα τιμή 8 bit.second: διεύθυνση μνήμης της δεύτερης σειράς, 16 δείγματα, καθένα τιμή 8 bit.first = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}
Αυτή η συνάρτηση δεν έχει τιμή επιστροφής.
Ένα πέρασμα φωτεινότητας κλιμακώνει κάθε δείγμα με ακρίβεια εργασίας 16 bit, ώστε ένα υπερβολικά φωτεινό δείγμα να μπορεί να ξεπεράσει το 255 και μια διαφορά να πέσει κάτω από το 0.
Το τελικό στάδιο συμπτύσσει αυτές τις τιμές εργασίας πίσω σε δείγματα 8 bit για εμφάνιση, περιορίζοντας καθετί κάτω από το 0 προς τα πάνω στο 0 και καθετί πάνω από το 255 προς τα κάτω στο 255.
Υλοποίησε τη συνάρτηση pack_samples, η οποία συμπτύσσει δύο ομάδες των 8 τιμών εργασίας σε μία σειρά 16 δειγμάτων, με τη σειρά.
Αυτή η συνάρτηση δέχεται ως ορίσματα, με αυτή τη σειρά:
result: διεύθυνση μνήμης για ένα buffer όπου γράφονται τα 16 περιορισμένα δείγματα, καθένα τιμή 8 bit.first: διεύθυνση μνήμης των πρώτων 8 τιμών εργασίας, καθεμία προσημασμένος ακέραιος 16 bit.second: διεύθυνση μνήμης των επόμενων 8 τιμών εργασίας, καθεμία προσημασμένος ακέραιος 16 bit.first = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200, 255, 255, 0, 100, 50, 255, 7, 0}
Αυτή η συνάρτηση δεν έχει τιμή επιστροφής.
Τα δεδομένα υφής και κορυφών συχνά φτάνουν διαπλεκόμενα, με το x και το y κάθε σημείου πακεταρισμένα μαζί.
Ωστόσο, συχνά είναι απαραίτητο να είναι χωριστά για αποδοτική επεξεργασία: όλες οι τιμές x σε ένα διάνυσμα, όλες οι τιμές y σε ένα άλλο.
Υλοποίησε τη συνάρτηση split_coordinates, η οποία χωρίζει τέσσερα διαπλεκόμενα σημεία (x, y) σε ένα διάνυσμα συντεταγμένων x και ένα διάνυσμα συντεταγμένων y.
Αυτή η συνάρτηση δέχεται ως ορίσματα, με αυτή τη σειρά:
xs: διεύθυνση μνήμης για ένα buffer όπου γράφονται οι 4 συντεταγμένες x, 4 αριθμοί κινητής υποδιαστολής 32 bit.ys: διεύθυνση μνήμης για ένα buffer όπου γράφονται οι 4 συντεταγμένες y, 4 αριθμοί κινητής υποδιαστολής 32 bit.first: διεύθυνση μνήμης των δύο πρώτων σημείων, 4 αριθμοί κινητής υποδιαστολής 32 bit, ως {x0, y0, x1, y1}.second: διεύθυνση μνήμης των δύο επόμενων σημείων, 4 αριθμοί κινητής υποδιαστολής 32 bit, ως {x2, y2, x3, y3}.first = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}
Αυτή η συνάρτηση δεν έχει τιμή επιστροφής.
Γράψου στο Exercism για να μάθεις και να κατακτήσεις x86-64 Assembly με 22 έννοιες130 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.