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.
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:
eq für Gleichheit und gt für Größer-als.
Andere Varianten werden durch Kombination des Ergebnisses von einem dieser Vergleiche gebildet.Abgesehen von der Verwendung eines bestimmten Bedingungssuffixes bei Ganzzahlvergleichen folgt die Syntax derselben Struktur, die wir bereits gesehen haben:
p + cmp + Bedingung + Größe (b, w, d oder q).cmp + p + Größe (s oder d).
Die Bedingung wird als zusätzlicher Operand in einem Immediate übergeben.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.
Zwei praktische Idiome sind:
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.
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 |
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.
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.
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.
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.
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:
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.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.
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.
Die Berechnungen in dieser Übung sollten mit SIMD-Befehlen durchgeführt werden.
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.
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.
Jede Punktzahl erhält einen Rang von 1 bis 3:
50.0.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.
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.
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.
Melde dich bei Exercism an, um x86-64 Assembly mit 22 Konzepte130 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.