Notenliste

Notenliste

Lernübung

Einführung

SIMD: Masken und Bedingungen

Skalarer Code verlässt sich auf Flags, die von verschiedenen Befehlen gesetzt werden, um als Reaktion auf eine bestimmte Bedingung zu verzweigen. Gepackte Werte hingegen repräsentieren nicht einen, sondern viele Werte parallel. Eine einzelne Bedingung kann für eine Lane fehlschlagen und für eine andere zutreffen.

Deshalb ist SIMD-Code standardmäßig verzweigungsfrei.

Anstatt sich auf Flags zu verlassen, erzeugen gepackte Vergleiche normalerweise eine Maske im Zieloperanden. Für jede Lane füllt der Vergleich die gesamte Lane mit Einsen, wenn sie wahr ist, und mit Nullen, wenn sie falsch ist. Als vorzeichenbehaftete Ganzzahl gelesen ist eine wahre Lane -1 und eine falsche Lane 0.

Diese Maske kann dann mit bitweisen Operationen kombiniert werden, um bestimmte Lanes zu filtern.

Gepackte Vergleiche

Der skalare Befehl cmp ist insofern generisch, als er verwendet wird, um verschiedene Flags gleichzeitig zu setzen. Ein anderer Befehl kann diese Flags dann verbrauchen, um entweder zu verzweigen oder Berechnungen durchzuführen.

Da ein gepackter Vergleich jedoch sowohl eine Bedingung prüft als auch eine Maske berechnet, ist er nicht generisch. Dem Vergleich muss die genaue Bedingung übergeben werden, die getestet wird.

Dafür gibt es zwei Möglichkeiten:

  • Ganzzahlvergleiche erhalten die Bedingung als Suffix: eq für Gleichheit und gt für Größer-als. Andere Varianten werden durch Kombination des Ergebnisses von einem dieser Vergleiche gebildet.
  • Gleitkommavergleiche erhalten die Bedingung in einem Immediate kodiert. Verschiedene Werte dieses Immediates entsprechen verschiedenen getesteten Bedingungen.

Abgesehen von der Verwendung eines bestimmten Bedingungssuffixes bei Ganzzahlvergleichen folgt die Syntax derselben Struktur, die wir bereits gesehen haben:

  • Für Ganzzahlen: p + cmp + Bedingung + Größe (b, w, d oder q).
  • Für Gleitkommazahlen: cmp + p + Größe (s oder d). Die Bedingung wird als zusätzlicher Operand in einem Immediate übergeben.
Ganzzahlvergleiche

Wie erwähnt gibt es nur Ganzzahlvergleiche für Gleichheit und Größer-als:

instruction description
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq Gleichheit pro Lane
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq vorzeichenbehaftetes Größer-als pro Lane
movdqa  xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0

Um einen Kleiner-als-Vergleich zu erzeugen, verwende gt mit vertauschten Operanden: a < b == b > a.

Beachte, dass der Vergleich vorzeichenbehaftet ist. Um einen vorzeichenlosen Vergleich durchzuführen, kippe das höchstwertige Bit beider Operanden. Das kannst du mit einem XOR mit einer Maske tun, in der nur das höchstwertige Bit gesetzt ist.

Note

Zwei praktische Idiome sind:

  1. Ein Register mit sich selbst per XOR zu verknüpfen, um lauter Nullen zu erzeugen.
  2. Ein Register mit sich selbst zu vergleichen, um lauter Einsen zu erzeugen.

Zum Beispiel:

pxor xmm4, xmm4    ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones

Lauter Nullen und lauter Einsen sind übliche Masken, um „überall falsch“ bzw. „überall wahr“ zu kodieren. Sie können auch verwendet werden, um gepackte 0 oder gepackte -1 darzustellen, was übliche Sentinel-Werte sind. Zum Beispiel ist das NUL, das das Ende eines Strings markiert, eine 0.

Gleitkommavergleiche

Gleitkomma-Lanes verwenden eine andere Form: ein Befehl, cmpps (und cmppd für 64-Bit-Lanes), mit der Bedingung als Immediate:

movaps xmm0, [rel readings]
cmpps  xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]

NASM hat auch Pseudo-Ops, die auf das richtige Immediate abbilden und leichter zu merken sind. In allem Folgenden kann x in px für s (32-Bit-Gleitkommazahlen) oder d (64-Bit-Gleitkommazahlen) stehen:

pseudo-op immediate comparison
cmpeqpx 0 a == b
cmpltpx 1 a < b
cmplepx 2 a <= b
cmpunordpx 3 a ist NaN oder b ist NaN
cmpneqpx 4 a != b
cmpnltpx 5 a >= b
cmpnlepx 6 a > b
cmpordpx 7 weder a noch b ist NaN

Auswählen mit einer Maske

Eine Maske kodiert das Ergebnis einer Bedingung. Sie kann dann verwendet werden, um gemäß dieser Bedingung Lane für Lane zwischen zwei Wertemengen zu wählen. Wir nehmen die Lane von einem Wert, wo die Maske wahr ist, und von einem anderen, wo sie falsch ist:

; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0  ; xmm0 holds the mask, keep a copy
pand   xmm2, xmm3  ; xmm2 = a AND mask: lanes of a where mask is true
pandn  xmm0, xmm4  ; xmm0 = NOT mask AND b: lanes of b where mask is false
por    xmm2, xmm0  ; combine the two halves

Beachte, dass sich die Asymmetrie von pandn hier auszahlt: Die Maske liegt im Ziel, wird negiert und wählt in einem einzigen Befehl aus b aus.

Dieses Muster ist die gepackte Form der verzweigungsfreien Auswahl. Jede Lane wird berechnet, und allein die Maske entscheidet, welcher Wert überlebt, ganz ohne jcc.

Dedizierte Blend-Befehle

Es gibt Befehle, die dieselbe Auswahl direkt ausführen und dabei ein Bit pro Element aus einem Maskenregister lesen. Sie heißen Blend-Befehle:

instruction element mask source
pblendvb Byte implizit xmm0
blendvps 32-Bit-Lane implizit xmm0
blendvpd 64-Bit-Lane implizit xmm0

Beachte, dass der erste Befehl der Ganzzahlsyntax folgt, während die anderen beiden der Gleitkommasyntax folgen. Da diese Befehle jedoch einfach rohe Bytes auswählen, kann jeder von ihnen sowohl mit Ganzzahlen als auch mit Gleitkommazahlen verwendet werden.

Für jedes Element behält der Blend das Ziel, wenn das höchstwertige Bit des zugehörigen Maskenelements nicht gesetzt ist, und übernimmt die Quelle, wenn es gesetzt ist. Nur dieses höchstwertige Bit wird herangezogen, was eine Vergleichsmaske erfüllt, da ihre Lanes entweder lauter Einsen oder lauter Nullen sind. Das Maskenregister ist immer xmm0, was implizit ist:

movaps   xmm0, [rel mask]  ; the selecting mask must be in xmm0
movaps   xmm1, [rel b]     ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a]     ; source: taken where the mask bit is set

Es ist auch möglich, pblendvb zu verwenden, um Lanes aus einer Vergleichsmaske für jede andere Größe auszuwählen. Da alle Bytes in einer wahren Lane lauter Einsen sind, wählt pblendvb alle davon aus.

Note

Diese Befehle hängen alle ein v nach der ausgeführten Operation an (blend). Dieses v steht für variabel, weil die Auswahl nicht statisch ist: Sie hängt von einem Register ab.

Es gibt auch Varianten ohne v, die gemäß einem Immediate auswählen. Sie folgen demselben Muster und wählen eine Lane i aus, wenn Bit i des Immediates gesetzt ist.

Von einer Maske zurück zu einem Skalar

So mächtig SIMD-Code auch ist, ihm fehlt viel von der Flexibilität von Skalarcode. In vielen Situationen ist es nötig, von einem gepackten Register zurück in die Welt der Skalarbefehle zu wechseln.

Die Befehlsfamilie movmsk dient als Brücke zwischen den beiden Welten. Diese Befehle extrahieren das höchstwertige Bit jeder Lane in ein Allzweckregister:

instruction gathers result width
pmovmskb höchstwertiges Bit jedes der 16 Bytes 16 Bit
movmskps höchstwertiges Bit jedes der 4 Dwords 4 Bit
movmskpd höchstwertiges Bit jedes der 2 Qwords 2 Bit

Nach einem Vergleich stellt jedes gesetzte Bit eine „wahre“ Lane dar und jedes gelöschte Bit eine „falsche“ Lane. Dieses Ergebnis kann dann wie gewohnt mit Skalarbefehlen weiterverarbeitet werden. Zum Beispiel zählt popcnt die Anzahl der Treffer, und tzcnt findet den ersten.

Das Allzweckregister kann 32 Bit oder 64 Bit breit sein.

Einen ganzen Vektor testen

Es gibt auch eine gepackte Variante des skalaren Befehls test: ptest.

Er ähnelt seinem skalaren Gegenstück darin, dass er eine AND-Operation zwischen zwei Operanden ausführt, ohne sie zu verändern. Anders als test führt ptest außerdem eine ANDN-Operation aus, die den ersten Operanden negiert.

Somit kann ptest als nicht-destruktive Version von pand und pandn betrachtet werden, die Flags entsprechend dem Ergebnis setzt. Ähnlich wie diese beiden Befehle behandelt ptest das gesamte SIMD-Register als eine einzige Lane und nimmt daher kein Größenpräfix.

Wenn das Ergebnis einer AND-Operation 0 ist, wird das ZF gesetzt, und wenn die ANDN-Operation 0 ergibt, wird das CF gesetzt. Das bedeutet, dass ptest verwendet werden kann, um sowohl auf eine Maske aus lauter Einsen als auch auf eine aus lauter Nullen zu prüfen:

  1. ptest auf ein Register mit sich selbst setzt ZF nur, wenn das Register lauter Nullen enthält. Dies ahmt das übliche skalare Idiom nach, test auf ein Register mit sich selbst anzuwenden, um auf 0 zu prüfen.
  2. ptest auf ein Register mit einer Maske aus lauter Einsen setzt CF nur, wenn das Register lauter Einsen enthält. Außerdem setzt es ZF nur, wenn das Register lauter Nullen enthält, was es möglich macht, beide Masken auf einmal zu prüfen.
pxor    xmm0, xmm0   ; all zeros
pcmpeqb xmm1, xmm1   ; all ones
pcmpeqb xmm2, xmm2

ptest xmm0, xmm0     ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1     ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1     ; CF is set only if xmm2 is all ones

Das Ergebnis eines ptest kann wie gewohnt zum Verzweigen oder in verzweigungsfreien Befehlen wie setcc oder cmovcc verwendet werden.

Anleitung

Du betreibst die Bewertungsstation einer Schule und wertest die Klassenergebnisse Block für Block aus.

Jeder Block enthält 4 Ergebnisse, und die Station wendet dieselbe Operation auf jedes Ergebnis im Block an. Eine Punktzahl ist eine 32-Bit-Gleitkommazahl. Mehrere Schritte arbeiten mit einer Maske: einem Block aus 4 Lanes, wobei jede Lane entweder nur Einsen (ein Ja für dieses Ergebnis) oder nur Nullen (ein Nein) enthält.

Du hast fünf Aufgaben. Die Operanden erhältst du über Speicheradressen. Manche Aufgaben schreiben ihre Antwort an eine Ergebnisadresse, andere geben sie direkt zurück.

Alle Speicheradressen in dieser Übung sind auf 16 Byte ausgerichtet.

Note

Die Berechnungen in dieser Übung sollten mit SIMD-Befehlen durchgeführt werden.

1. Markiere die Punktzahlen über dem Schwellenwert

Der erste Schritt bewertet jedes Ergebnis anhand eines Schwellenwerts. Ein Ergebnis schafft die Hürde, wenn seine Punktzahl strikt größer als der Schwellenwert ist. Jede Punktzahl, die kleiner oder gleich dem Schwellenwert ist, schafft sie nicht.

Implementiere die Funktion flag_above_threshold, die eine Maske aufbaut: eine Lane aus lauter Einsen für jede Punktzahl über ihrem Schwellenwert und eine Lane aus lauter Nullen andernfalls.

Diese Funktion erhält folgende Argumente, in dieser Reihenfolge:

  • result: Speicheradresse für einen Puffer, in den die 4 Masken-Lanes geschrieben werden.
  • scores: Speicheradresse der Punktzahlen, mit 4 normalen 32-Bit-Gleitkommazahlen (niemals NaN).
  • thresholds: Speicheradresse des Schwellenwerts für jede Lane, mit 4 normalen 32-Bit-Gleitkommazahlen (niemals NaN).
scores     = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result     = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

Diese Funktion hat keinen Rückgabewert.

2. Markiere die perfekten Punktzahlen

Ein separater Bericht hebt die perfekten Ergebnisse hervor, also die, die die maximal mögliche Punktzahl erreicht haben.

Implementiere die Funktion flag_perfect, die eine Maske aufbaut: eine Lane aus lauter Einsen für jede Punktzahl, die gleich ihrer Höchstpunktzahl ist, und eine Lane aus lauter Nullen andernfalls.

Diese Funktion erhält folgende Argumente, in dieser Reihenfolge:

  • result: Speicheradresse für einen Puffer, in den die 4 Masken-Lanes geschrieben werden.
  • scores: Speicheradresse der Punktzahlen, mit 4 normalen 32-Bit-Gleitkommazahlen (niemals NaN).
  • maxima: Speicheradresse der Höchstpunktzahl für jede Lane, mit 4 normalen 32-Bit-Gleitkommazahlen (niemals NaN).
scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

Diese Funktion hat keinen Rückgabewert.

3. Vergib einen Rang

Jede Punktzahl erhält einen Rang von 1 bis 3:

  • Rang 1 für eine Punktzahl auf oder unter der Bestehensschwelle von 50.0.
  • Rang 2 für eine Punktzahl über dieser Schwelle, aber unter der Höchstpunktzahl.
  • Rang 3 für eine perfekte Punktzahl, also eine, die gleich der Höchstpunktzahl ist.

Implementiere die Funktion assign_ranks, die den Rang jeder Punktzahl schreibt.

Du solltest die Bestehensschwelle und die Rangwerte als gepackte Konstanten im Speicher definieren. Die Funktionen aus den beiden vorherigen Aufgaben können wiederverwendet werden: Eine Punktzahl hat mindestens Rang 2, wenn sie über der Schwelle liegt, und Rang 3, wenn sie gleich der Höchstpunktzahl ist.

Diese Funktion erhält folgende Argumente, in dieser Reihenfolge:

  • result: Speicheradresse für einen Puffer, in den die 4 Ränge geschrieben werden, jeder als 32-Bit-Ganzzahl ohne Vorzeichen.
  • scores: Speicheradresse der Punktzahlen, mit 4 normalen 32-Bit-Gleitkommazahlen (niemals NaN).
  • maxima: Speicheradresse der Höchstpunktzahl für jede Lane, mit 4 normalen 32-Bit-Gleitkommazahlen (niemals NaN).
scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}

Diese Funktion hat keinen Rückgabewert.

4. Zähle die Fehlschläge

Im Laufe des Jahres bauen die Lernenden einen Gesamtrang auf. Die Station zählt, wie viele Ränge in der gesamten Kohorte unter eine Bestehensschwelle fallen, um zu planen, wie viele zusätzliche Unterrichtsstunden nötig sind.

Implementiere die Funktion count_failures, die zurückgibt, wie viele Ränge über alle Blöcke hinweg strikt unter der Bestehensschwelle liegen. Die Schwelle wird als Block aus 4 identischen Lanes übergeben, sodass du sie einmal laden und für jeden Block wiederverwenden kannst.

Diese Funktion erhält folgende Argumente, in dieser Reihenfolge:

  • ranks: Speicheradresse der Ränge, eine ganze Anzahl von 4-Lane-Blöcken, jeder Rang eine 32-Bit-Ganzzahl ohne Vorzeichen.
  • block_count: die Anzahl der 4-Lane-Blöcke, immer größer als 0.
  • pass_threshold: Speicheradresse der Bestehensschwelle, mit 4 identischen 32-Bit-Ganzzahlen.
ranks          = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count    = 2
pass_threshold = {2, 2, 2, 2}
// => 3

Diese Funktion gibt die Anzahl als vorzeichenbehaftete 32-Bit-Ganzzahl zurück.

5. Haben alle bestanden?

Bevor die Aufzeichnungen abgelegt werden, prüft die Station, ob die Kohorte sauber ist: Sie besteht die Prüfung, wenn in keinem Block auch nur ein einziges Ergebnis durchgefallen ist.

Implementiere die Funktion all_passed, die 1 zurückgibt, wenn alle bestanden haben, und sonst 0. Bestanden hat, wessen entsprechende Lane im Array failing nur Nullen enthält.

Diese Funktion erhält folgende Argumente, in dieser Reihenfolge:

  • failing: Speicheradresse der Fehlermasken, eine ganze Anzahl von 4-Lane-Blöcken, jede Lane nur Einsen oder nur Nullen.
  • block_count: die Anzahl der 4-Lane-Blöcke, immer größer als 0.
failing     = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
               0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1

Diese Funktion gibt die Antwort als vorzeichenbehaftete 32-Bit-Ganzzahl zurück, entweder 1 oder 0.

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

Bereit, mit Notenliste 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.