Bisher hat jede SIMD-Operation Lane für Lane gearbeitet.
Der Wert in Lane i des Ergebnisses wird auf der Lane i der Eingabewerte berechnet.
Allerdings gibt es viele Situationen, in denen das Verschieben von Werten zwischen Lanes notwendig oder gewünscht ist. Zum Beispiel können die Lanes für die Berechnung, die wir durchführen müssen, in der falschen Reihenfolge vorliegen.
Es gibt viele SIMD-Instruktionen, die Daten auf unterschiedliche Weise über Lanes hinweg verschieben.
Ein Shuffle ordnet die Bytes oder Lanes eines Registers neu, wobei jede Ziel-Lane aus einer Quell-Lane stammt, die sich irgendwo befinden kann. Sie können dieselbe Lane verschiedenen Zielen zuweisen, wodurch sie auch Werte broadcasten können.
Die Shuffle-Instruktionen verhalten sich je nach ihrer Größe und Ausführungsdomäne unterschiedlich.
Die Instruktion pshufd ordnet die vier 32-Bit-Lanes ihrer Quelle neu in ihr Ziel ein.
Die Auswahl erfolgt über ein 8-Bit-Immediate, das als vier 2-Bit-Felder gelesen wird, eines pro Ziel-Lane. Jedes Feld wählt aus, welche der vier Quell-Lanes in diese Ziel-Lane kopiert wird:
| Feld | Lane-Index |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
Die Position des Feldes im Immediate, von rechts nach links gelesen, gibt an, wo die ausgewählte Lane eingefügt wird. Eine Quell-Lane kann mehr als einmal ausgewählt werden; so wird eine einzelne Lane auf das gesamte Register gebroadcastet:
; 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
Es gibt zwei Gleitkomma-Shuffle-Instruktionen, die derselben allgemeinen Syntax folgen: shuf + p + das Größen-Suffix (entweder s oder d).
Sie verwenden ebenfalls ein Immediate, um die Lanes auszuwählen.
shufps verwendet dieselbe 2-Bit-Feldkodierung wie die vorherigen Ganzzahl-Shuffles.
Da ein 128-Bit-Operand jedoch nur zwei 64-Bit-Lanes enthält, verwendet shufpd nur eine 1-Bit-Feldkodierung.
Diese Instruktionen unterscheiden sich von ihren Ganzzahl-Gegenstücken darin, dass sie die Lanes aus zwei Operanden statt aus einem beziehen:
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]}
Da beide Operanden in das Ergebnis einfließen, können sie verwendet werden, um zwei Vektoren in einem Schritt zu verschachteln. Wenn Quelle und Ziel dasselbe Register sind, wird jede Lane daraus bezogen.
pshufb ist der allgemeinste Shuffle.
Obwohl pshufb und pshufd sehr ähnliche Namen haben und sich nur im Größen-Suffix unterscheiden, führen sie sehr unterschiedliche Operationen aus.
Erstens: Während pshufd ein Immediate verwendet, um Lane-Positionen auszuwählen, verwendet pshufb einen Steuervektor im Quelloperanden.
Dieser Steuervektor ist ein xmm-Register oder ein 16-Byte-Speicheroperand.
Für jede der 16 Ziel-Lanes geben die unteren vier Bits des Steuervektors einen Quell-Byte-Index von 0 bis 15 an.
Wenn Lane i des Steuervektors den Quell-Byte-Index j enthält, dann wird die j-th Lane der Quelle in die i-th Lane des Ziels verschoben.
Das zeigt einen zweiten Unterschied zwischen den beiden Instruktionen.
Während pshufd die zu shuffelnden Lanes aus einem anderen Quelloperanden bezieht, führt pshufb das Shuffeln direkt im Ziel durch.
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 ist sogar noch flexibler: Es kann beliebige Lanes löschen.
Wenn das höchste Bit in einer Lane i des Steuervektors gesetzt ist, wird die Lane i des Ziels auf null gesetzt.
Dadurch ist pshufb sowohl eine beliebige Byte-Permutation als auch ein selektives Löschen in einer einzigen Instruktion.
Da pshufb auf Byte-Granularität arbeitet, kann es auch verwendet werden, um Lanes unterschiedlicher Größe zu shuffeln, indem benachbarte Lanes gruppiert werden.
Zum Beispiel kann es verwendet werden, um Dwords zu shuffeln:
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}
Die Unpack-Instruktionen nehmen Lanes aus zwei Operanden und verweben sie miteinander, wobei sie zwischen beiden abwechseln. Es gibt Ganzzahl- und Gleitkomma-Varianten, die weitgehend derselben allgemeinen Syntaxstruktur folgen, mit zwei Unterschieden:
l oder h, das angibt, ob die Operation auf der unteren Hälfte (l) oder der oberen Hälfte (h) jedes Operanden arbeitet.bw oder qdq (das Größen-Suffix für 16 Bytes ist dq, wie in 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]}
Die Pack-Instruktionen arbeiten in die andere Richtung und kombinieren die Lanes zweier Operanden zu halbbreiten Lanes des Ziels.
Diese Instruktionen haben kein Präfix p.
Ansonsten kombiniert die Syntax Elemente, die wir bereits gesehen haben:
pack.s oder u, um anzugeben, ob die Ausgabewerte vorzeichenbehaftet oder vorzeichenlos sind.s für sättigend, wie bei der sättigenden Arithmetik aus einem früheren Konzept.unpck).Da die Pack-Operation verengend ist, benötigt sie kein Suffix l oder 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)
Diese Instruktionen sind sättigend, d. h., die Ausgabewerte werden auf den engeren Bereich begrenzt. Sie arbeiten von Dword zu Word und von Word zu Byte. Es gibt keine Variante von Qword zu Dword.
Beachte, dass die Eingabe immer als vorzeichenbehaftet interpretiert wird.
Der Typ, ob vorzeichenbehaftet oder vorzeichenlos, gilt für die Ausgabe.
Er gibt den Bereich an, auf den begrenzt wird.
Zum Beispiel begrenzt packsswb auf den Bereich eines vorzeichenbehafteten Bytes, d. h. [-128, 127].
Die unteren Lanes des Ergebnisses stammen aus dem Zieloperanden und die oberen Lanes aus der Quelle:
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]}
Für diese Instruktionen gibt es kein Gleitkomma-Äquivalent.
Bisher haben wir Daten immer mit movq/movd zwischen einem SIMD-Register und einem Allzweckregister verschoben.
Diese Instruktionen können nur in die untere Lane eines SIMD-Registers schreiben oder daraus lesen, und beim Schreiben löschen sie alle anderen Lanes.
Es gibt Instruktionen, die dasselbe für jede beliebige Lane tun, nicht nur für die erste, und dabei die anderen Lanes unberührt lassen:
Beide folgen der Ganzzahl-Syntax: p + insr/extr + das Größen-Suffix (b, w, d oder q).
In beiden Fällen ist das Allzweckregister normalerweise 32 Bit breit.
Nur pinsrq und pextrq benötigen einen 64-Bit-Operanden.
Ein Speicheroperand hat immer die Größe der Operation: 8 Bit für pinsrb/pextrb, 16 Bit für pinsrw/pextrw und so weiter.
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
Beachte, dass diese Instruktionen, ähnlich wie movd und movq, einfach rohe Bytes verschieben.
Das bedeutet, dass sie auch für Gleitkommawerte verwendet werden können, die im Speicher oder in einem Allzweckregister liegen.
Du schreibst die inneren Schleifen einer Software-Bildpipeline, also die Stufe, die Texturen vorbereitet und Ebenen zusammensetzt, bevor sie auf dem Bildschirm erscheinen. Die Pipeline arbeitet blockweise an Pixeln und wendet dieselbe Operation auf den gesamten Block an.
Ein Pixel besteht aus vier 1-Byte-Kanälen: Rot, Grün, Blau und Alpha, in dieser Reihenfolge (RGBA).
Ein Block besteht aus 4 Pixeln, also insgesamt 16 Bytes.
Du hast fünf Aufgaben.
Du erhältst die Operanden über Speicheradressen und schreibst dein Ergebnis über eine Ergebnisadresse zurück. Alle Speicheradressen in dieser Übung sind auf 16 Bytes ausgerichtet.
Die Berechnungen in dieser Übung sollten mit SIMD-Instruktionen durchgeführt werden, nicht mit skalaren Operationen.
Texturen werden als RGBA gespeichert, aber der Framebuffer, in den diese Pipeline zeichnet, erwartet jedes Pixel in der Reihenfolge BGRA: Rot und Blau vertauscht, Grün und Alpha an ihrem Platz.
Ein Bild kommt als Folge von Blöcken an, und jeder Block wird auf dieselbe Weise umgewandelt.
Implementiere die Funktion to_display_order, die ein ganzes Bild Block für Block von RGBA nach BGRA umwandelt.
Du solltest die Steuermaske für die Kanalumordnung als gepackte Konstante im Speicher definieren und sie für jeden Block wiederverwenden.
Diese Funktion nimmt die folgenden Argumente entgegen, in dieser Reihenfolge:
result: Speicheradresse für einen Puffer, in den die umgewandelten Blöcke geschrieben werden, 16 Bytes pro Block.pixels: Speicheradresse der Quellblöcke, 4 Pixel pro Block, jedes Pixel 4 Bytes in der Reihenfolge RGBA.block_count: die Anzahl der Blöcke, immer größer als 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}
Diese Funktion hat keinen Rückgabewert.
Um einen Bereich zu leeren oder eine einfarbige Fläche zu malen, schreibt die Pipeline eine einzige Farbe über jedes Pixel des Bereichs.
Implementiere die Funktion fill_region, die einen Bereich von block_count Blöcken mit Kopien einer Farbe füllt.
Diese Funktion nimmt die folgenden Argumente entgegen, in dieser Reihenfolge:
result: Speicheradresse für einen Puffer, in den die gefüllten Blöcke geschrieben werden, 16 Bytes pro Block.color: Speicheradresse eines Pixels, 4 Bytes in der Reihenfolge RGBA.block_count: die Anzahl der zu füllenden Blöcke, immer größer als 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}
Diese Funktion hat keinen Rückgabewert.
Zwei einkanalige Ebenen müssen zu einem Puffer zusammengeführt werden, wobei ihre Samples verschachtelt werden. Das Ergebnis wechselt sich ab: ein Sample aus der ersten Ebene, dann eines aus der zweiten.
Implementiere die Funktion weave_scanlines, die zwei Reihen mit je 16 Samples zu einer einzigen Reihe mit 32 Samples verschachtelt.
Diese Funktion nimmt die folgenden Argumente entgegen, in dieser Reihenfolge:
result: Speicheradresse für einen Puffer, in den die 32 verschachtelten Samples geschrieben werden.first: Speicheradresse der ersten Reihe, 16 Samples, jedes ein 8-Bit-Wert.second: Speicheradresse der zweiten Reihe, 16 Samples, jedes ein 8-Bit-Wert.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}
Diese Funktion hat keinen Rückgabewert.
Ein Helligkeitsdurchlauf skaliert jedes Sample in 16-Bit-Arbeitsgenauigkeit, sodass ein zu helles Sample 255 überschreiten und eine Differenz unter 0 fallen kann.
Die letzte Stufe verengt diese Arbeitswerte wieder auf 8-Bit-Samples für die Anzeige und begrenzt alles unter 0 auf 0 und alles über 255 auf 255.
Implementiere die Funktion pack_samples, die zwei Gruppen von 8 Arbeitswerten der Reihe nach zu einer Zeile mit 16 Samples verengt.
Diese Funktion nimmt die folgenden Argumente entgegen, in dieser Reihenfolge:
result: Speicheradresse für einen Puffer, in den die 16 begrenzten Samples geschrieben werden, jedes ein 8-Bit-Wert.first: Speicheradresse der ersten 8 Arbeitswerte, jeder eine 16-Bit-Ganzzahl mit Vorzeichen.second: Speicheradresse der nächsten 8 Arbeitswerte, jeder eine 16-Bit-Ganzzahl mit Vorzeichen.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}
Diese Funktion hat keinen Rückgabewert.
Textur- und Vertexdaten kommen oft verschachtelt an, wobei x und y jedes Punkts zusammen gepackt sind.
Oft ist es jedoch notwendig, sie für eine effiziente Verarbeitung zu trennen: alle x-Werte in einem Vektor, alle y-Werte in einem anderen.
Implementiere die Funktion split_coordinates, die vier verschachtelte (x, y)-Punkte in einen Vektor von x-Koordinaten und einen Vektor von y-Koordinaten trennt.
Diese Funktion nimmt die folgenden Argumente entgegen, in dieser Reihenfolge:
xs: Speicheradresse für einen Puffer, in den die 4 x-Koordinaten geschrieben werden, 4 32-Bit-Gleitkommazahlen.ys: Speicheradresse für einen Puffer, in den die 4 y-Koordinaten geschrieben werden, 4 32-Bit-Gleitkommazahlen.first: Speicheradresse der ersten zwei Punkte, 4 32-Bit-Gleitkommazahlen, als {x0, y0, x1, y1}.second: Speicheradresse der nächsten zwei Punkte, 4 32-Bit-Gleitkommazahlen, als {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}
Diese Funktion hat keinen Rückgabewert.
Melde dich bei Exercism an, um x86-64 Assembly mit 22 Konzepte130 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.