Kanal-Swizzle

Kanal-Swizzle

Lernübung

Einführung

SIMD: Lane-übergreifende Operationen

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.

Shuffeln

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.

Lanes mit einem Immediate auswählen

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
Shuffeln von Gleitkomma-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:

  • Die untere Hälfte des Ergebnisses stammt aus dem Zieloperanden.
  • Die obere Hälfte stammt aus dem Quelloperanden.
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.

Der Byte-Shuffle

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}

Lanes verschachteln

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:

  1. Es gibt ein Suffix l oder h, das angibt, ob die Operation auf der unteren Hälfte (l) oder der oberen Hälfte (h) jedes Operanden arbeitet.
  2. Die Ganzzahl-Varianten nehmen zwei Größen-Suffixe, wobei das zweite doppelt so groß ist wie das erste. Zum Beispiel 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]}

Verengung mit Sättigung

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:

  • die ausgeführte Operation, pack.
  • ein s oder u, um anzugeben, ob die Ausgabewerte vorzeichenbehaftet oder vorzeichenlos sind.
  • ein s für sättigend, wie bei der sättigenden Arithmetik aus einem früheren Konzept.
  • zwei Größen-Suffixe, wobei das zweite die halbe Breite des ersten angibt (das entgegengesetzte Verhalten von 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.

Einzelne Lanes verschieben

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:

  • Die Insert-Instruktionen schreiben ein Element aus einem Allzweckregister oder dem Speicher in eine Lane, die durch ein Immediate ausgewählt wird.
  • Die Extract-Instruktionen lesen aus einer Lane heraus in ein Allzweckregister.

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.

Anleitung

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.

Note

Die Berechnungen in dieser Übung sollten mit SIMD-Instruktionen durchgeführt werden, nicht mit skalaren Operationen.

1. Ein Bild in die Anzeigereihenfolge umwandeln

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.

2. Einen Bereich mit einer Farbe füllen

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.

3. Zwei Scanlines verweben

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.

4. Eine aufgehellte Zeile packen

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.

5. Koordinaten in x und y aufteilen

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.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
x86-64 Assembly Exercism

Bereit, mit Kanal-Swizzle zu starten?

Melde dich bei Exercism an, um x86-64 Assembly mit 22 Konzepte130 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.