Mischpult

Mischpult

Lernübung

Einführung

SIMD: Ganzzahlen

Ein früheres Konzept hat gepackte Gleitkommazahlen eingeführt: mehrere Zahlen in einem einzigen xmm-Register, die Lane-weise und parallel verarbeitet werden. Dieselben xmm-Register können auch gepackte Ganzzahlen aufnehmen.

Syntax

Das meiste vom SIMD-Gleitkommamodell überträgt sich unverändert auf gepackte Ganzzahlen:

  • Ein 128-Bit-Register wird in Lanes aufgeteilt
  • Befehle wirken parallel auf Lanes an derselben Position
  • Speicheroperanden folgen denselben 16-Byte-Ausrichtungsregeln

Die Syntax ist allerdings etwas anders:

  1. Ein Präfix p, das angibt, dass der Befehl auf gepackten Daten arbeitet.
  2. Die ausgeführte Operation, mit demselben Namen wie ihr Nicht-SIMD-Gegenstück (auch skalar genannt) (z. B. add, mul usw.).
  3. Ein Suffix, das die Größe jeder Lane angibt.

Gepackte Ganzzahlen haben vier Lane-Größen, und jede hat ihr eigenes Suffix:

Lane-Breite Bytes Lanes in 128 Bit Suffix
Byte 1 16 b
Word 2 8 w
Dword 4 4 d
Qword 8 2 q

Zum Beispiel:

Befehl Bedeutung
paddb gepacktes add, 8-Bit-Lanes (16 Lanes)
paddw gepacktes add, 16-Bit-Lanes (8 Lanes)
paddd gepacktes add, 32-Bit-Lanes (4 Lanes)
paddq gepacktes add, 64-Bit-Lanes (2 Lanes)

Manche Befehle nehmen Lanes mit einer Größe als Eingabe, geben aber eine andere Größe als Ausgabe aus. Sie folgen derselben allgemeinen Konvention, aber mit zwei Größensuffixen. Das erste gibt die Größe der Eingabe-Lane an, das zweite die Größe der Ausgabe-Lane:

Befehl Bedeutung
pmovsxwd gepacktes movsx, von 16-Bit-Lanes zu 32-Bit-Lanes
pmuldq gepacktes mul, von 32-Bit-Lanes zu 64-Bit-Lanes

Speicherbewegungen

Zwei Befehle, die nach Ganzzahlen benannt sind, kopieren 128 Bit zwischen einem xmm-Register und dem Speicher:

Befehl Beschreibung
movdqa gepackte Ganzzahlen von oder zu einer ausgerichteten Speicherstelle kopieren
movdqu gepackte Ganzzahlen von oder zu einer nicht ausgerichteten Speicherstelle kopieren

Sie verhalten sich wie movaps und movups: movdqa löst bei einer fehlausgerichteten Adresse einen Fehler aus, movdqu akzeptiert jede. Alle vier kopieren 128 Bit, ohne sie zu interpretieren. Das nach Ganzzahlen benannte Paar wird aus Konvention mit Ganzzahldaten verwendet, nicht aus Notwendigkeit.

Note

dq steht hier für double-qword, d. h. 128 Bit (16 Byte).

Addition/Subtraktion

Addition und Subtraktion folgen der Namensregel:

paddb xmm0, xmm1 ; 16 lanes: each  8-bit, xmm0 += xmm1
paddw xmm2, xmm3 ;  8 lanes: each 16-bit, xmm2 += xmm3

psubd xmm4, xmm5 ;  4 lanes: each 32-bit, xmm4 -= xmm5
psubq xmm6, xmm7 ;  2 lanes: each 64-bit, xmm6 -= xmm7

Es gibt keine getrennte Form für vorzeichenbehaftet und vorzeichenlos. Im Zweierkomplement erzeugen Addition und Subtraktion dieselben Bits, egal ob die Lanes als vorzeichenbehaftet oder vorzeichenlos gelesen werden, deshalb kommt ein einziger Befehl für beide aus. Die Interpretation liegt bei dir, genau wie bei den skalaren add und sub.

Diese Befehle umlaufen bei Überlauf, wie ihre skalaren Gegenstücke. Eine 8-Bit-Lane hält Werte modulo 256, also ergibt ein paddb von 200 + 100 den Wert 300 - 256 = 44, wobei die nicht passenden Bits verworfen werden.

Beachte, dass diese zusätzlichen Bits nicht in die nächste Lane übergehen. Jede Lane wird unabhängig von den anderen verarbeitet, obwohl sie dasselbe Register teilen.

Sättigende Addition/Subtraktion

SIMD für Ganzzahlen fügt eine Operation hinzu, die SIMD für Gleitkommazahlen nicht hat: sättigende Addition und Subtraktion, die begrenzen statt umzulaufen. Ein Ergebnis über dem Bereich der Lane wird der größte Wert, den die Lane halten kann; ein Ergebnis unter dem Bereich wird der kleinste.

Die sättigenden Formen fügen s (vorzeichenbehaftet) oder us (vorzeichenlos) vor dem Größensuffix ein:

Befehl Bedeutung
paddsb add, sättigend, vorzeichenbehaftete 8-Bit-Lanes
paddusb add, sättigend, vorzeichenlose 8-Bit-Lanes
psubsw sub, sättigend, vorzeichenbehaftete 16-Bit-Lanes
psubusw sub, sättigend, vorzeichenlose 16-Bit-Lanes

Der Begrenzungsbereich ist der volle darstellbare Bereich für eine Ganzzahl der entsprechenden Größe und Vorzeichenbehaftung. Für ein Byte:

  • Vorzeichenlose Bytes werden auf [0, 255] begrenzt: Ein paddusb von 200 + 100 ergibt 255, und ein psubusb von 5 - 10 ergibt 0.
  • Vorzeichenbehaftete Bytes werden auf [-128, 127] begrenzt: Ein paddsb von 100 + 50 ergibt 127.

Sättigung ist wichtig, wenn eine Lane eine begrenzte Größe enthält, etwa einen Pixelkanal oder einen Abtastwert. Ein umlaufender Wert würde ein zu helles Pixel dunkel machen, während die Begrenzung es auf maximaler Helligkeit hält, was das gewünschte Ergebnis ist.

Note

Sättigende Addition und Subtraktion gibt es nur für Byte- und Word-Lanes, nicht für Dword- oder Qword-Lanes.

Multiplikation

Die Multiplikation zweier N-Bit-Werte kann ein 2N-Bit-Produkt ergeben, aber die Ziel-Lane ist nur N Bit breit. SIMD-Operationen lösen das, indem sie angeben, welche Hälfte des Produkts behalten wird, entweder die unteren N Bits oder die oberen N Bits.

Für 16-Bit-Lanes werden drei Befehle verwendet:

Befehl Bedeutung
pmullw mul, 16-Bit-Lanes, die unteren 16 Bits jedes Produkts behalten
pmulhw mul, 16-Bit-Lanes, die oberen 16 Bits behalten, vorzeichenbehaftete Operanden
pmulhuw mul, 16-Bit-Lanes, die oberen 16 Bits behalten, vorzeichenlose Operanden

Die unteren 16 Bits eines Produkts sind gleich, egal ob die Operanden als vorzeichenbehaftet oder vorzeichenlos gelesen werden, deshalb gibt es nur ein einziges pmullw.

Die oberen 16 Bits hängen jedoch von der Vorzeichenbehaftung des Ergebnisses ab. Deshalb hat die Multiplikation der oberen Hälfte getrennte Formen für vorzeichenbehaftete und vorzeichenlose Multiplikation:

  1. pmulhw, für vorzeichenbehaftete Multiplikation.
  2. pmulhuw, mit einem zusätzlichen u, für vorzeichenlose Multiplikation.

Beachte die Syntax:

  1. Ein p, für gepackte Ganzzahl.
  2. Die ausgeführte Operation, mul.
  3. Ein h, das angibt, dass die oberen („hohen“) Bits des Ergebnisses ausgewählt werden.
  4. Ein optionales u, wenn das Ergebnis als vorzeichenlos interpretiert werden soll (d. h. es wird nicht vorzeichenerweitert).
  5. Schließlich das Größensuffix w, das angibt, dass es sich um eine Word-Operation (16 Bit) handelt.

Die gepackte Multiplikation für Words folgt genau der obigen Regel. Die Dword-Multiplikation (32 Bit) folgt ebenfalls der Regel, aber es gibt nur die Variante, die die untere Hälfte auswählt: pmulld.

Es gibt keine Variante der Dword-Multiplikation, die die oberen Bits auswählt. Es gibt jedoch Varianten, die die Multiplikation verbreitern und das volle 64-Bit-Produkt von geradzahlig indizierten Lanes speichern (d. h. der Lanes an Position 0 und 2):

Befehl Bedeutung
pmuludq die geradzahlig indizierten 32-Bit-Elemente vorzeichenlos multiplizieren, in 2 volle 64-Bit-Produkte
pmuldq die geradzahlig indizierten 32-Bit-Elemente vorzeichenbehaftet multiplizieren, in 2 volle 64-Bit-Produkte

Beachte, dass die Syntax dq verwendet, gegebenenfalls mit einem u davor für vorzeichenlose Multiplikation. Das liegt daran, dass die Befehle dword-Lanes als Eingabe nehmen und qword-Lanes ausgeben.

Division

Es gibt keine Division für gepackte Ganzzahlen. Code, der sie braucht, wandelt die Werte in Gleitkommazahlen um, dividiert und wandelt sie zurück.

Ganzzahlen verbreitern

Es gibt gepackte Entsprechungen für movsx und movzx. Sie folgen derselben Syntax, die wir für Befehle erwähnt haben, deren Eingaben eine andere Lane-Größe haben als ihre Ausgaben:

pmovsxwd xmm0, xmm1   ; 4 words -> 4 dwords, sign-extended
pmovzxbw xmm0, xmm1   ; 8 bytes -> 8 words, zero-extended

Beachte, dass die Anzahl der Lanes durch die größere (Ausgabe-)Breite festgelegt wird. Der Befehl liest so viele Lanes aus dem unteren Teil der Quelle. Das ist dieselbe Semantik, die wir bereits bei gepackten cvt-Befehlen zwischen Gleitkommazahlen mit einfacher und doppelter Genauigkeit gesehen haben.

Zwischen Ganzzahlen und Gleitkommazahlen konvertieren

Es gibt Befehle, um zwischen vorzeichenbehafteten 32-Bit-Ganzzahlen und Gleitkomma-Lanes zu konvertieren. Sie folgen der üblichen Syntax für cvt- und cvtt-Befehle, aber statt si wird eine gepackte Ganzzahl durch dq dargestellt:

cvtdq2ps xmm0, xmm1 ; convert 32-bit signed integers in xmm1 to 32-bit floats in xmm0
cvtdq2pd xmm2, xmm3 ; convert 32-bit signed integers in xmm3 to 64-bit floats in xmm2
cvtps2dq xmm4, xmm5 ; convert 32-bit floats in xmm5 to 32-bit signed integers in xmm4
Caution

Die Befehle, die pi verwenden, um gepackte Ganzzahlen darzustellen, schreiben in veraltete mmx-Register und sind auf x86-64 praktisch veraltet. Bevorzuge die dq-Formen, die xmm-Register verwenden.

Dieselben Anmerkungen, die zur skalaren Konvertierung zwischen Gleitkommazahlen und Ganzzahlen gemacht wurden, gelten auch hier. Gleitkommazahlen werden gemäß einem speziellen Register namens MXCSR gerundet, dessen Modus du beim Eintritt in die Funktion nicht voraussetzen kannst. Es gibt eine cvtt-Variante (mit einem zusätzlichen t), die das Ergebnis immer abschneidet.

Es ist auch möglich, round zu verwenden, um gepackte Gleitkommawerte vor der Konvertierung in einen bekannten Zustand zu bringen. Der Rundungssteuerwert ist derselbe wie beim skalaren round, und die Syntax ist für gepackte Gleitkommawerte ebenfalls dieselbe:

roundps xmm0, xmm1, 1 ; xmm0 = floor(xmm1), packed 32-bit floats
roundpd xmm2, xmm3, 2 ; xmm2 = ceil(xmm3), packed 64-bit floats

Anleitung

Du schreibst den digitalen Kern eines kleinen Mischpults in einem Tonstudio.

Ton kommt als Strom von Samples an. Jedes Sample ist eine Zahl, die die Position der Lautsprechermembran zu einem Zeitpunkt beschreibt. Ein Mischpult verarbeitet einen ganzen Block von Samples auf einmal und wendet auf jedes dieselbe Operation an.

Du hast fünf Aufgaben, die jeweils auf einem im Speicher liegenden Block von Samples arbeiten. Du erhältst die Operanden über Speicheradressen und schreibst dein Ergebnis über eine Ergebnisadresse.

Note

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

1. Zwei Spuren mischen

Das Studio nimmt Instrumente manchmal auf verschiedenen Spuren auf. Um sie über dieselben Lautsprecher abzuspielen, summierst du die beiden Spuren zu einer.

Implementiere die Funktion mix_tracks, die zwei Spuren Sample für Sample addiert. Jede Spur ist ein Block aus 8 vorzeichenbehafteten 16-Bit-Samples. Eine Summe, die den vorzeichenbehafteten 16-Bit-Bereich überläuft, muss begrenzt werden statt umzuschlagen.

Diese Funktion nimmt folgende Argumente in dieser Reihenfolge entgegen:

  • result: 16-Byte-ausgerichtete Speicheradresse für einen Puffer, in den die 8 gemischten Samples geschrieben werden.
  • track_a: 16-Byte-ausgerichtete Speicheradresse der ersten Spur mit 8 vorzeichenbehafteten 16-Bit-Samples.
  • track_b: 16-Byte-ausgerichtete Speicheradresse der zweiten Spur mit 8 vorzeichenbehafteten 16-Bit-Samples.
track_a = {10000, 30000, 20000, -30000, -20000, -100,  100, 0}
track_b = { 5000,  5000, 20000,  -5000, -20000,  100, -100, 0}
result  = {15000, 32767, 32767, -32768, -32768,    0,    0, 0}

Diese Funktion hat keinen Rückgabewert.

2. Übersprechen entfernen

Wenn zwei Mikrofone gleichzeitig aufnehmen, fängt jedes eine schwache Kopie der Quelle des anderen auf. Diese unerwünschte Kopie heißt Übersprechen. Sobald dieses Übersprechen gemessen wurde, muss es wieder abgezogen werden, um die Spur zu bereinigen.

Implementiere die Funktion remove_bleed, die den Übersprechblock Sample für Sample vom Spurblock subtrahiert. Jeder Block besteht aus 8 vorzeichenbehafteten 16-Bit-Samples, und die Subtraktion muss sättigen.

Diese Funktion nimmt folgende Argumente in dieser Reihenfolge entgegen:

  • result: Speicheradresse, an die die 8 bereinigten Samples geschrieben werden (Ausrichtung nicht garantiert).
  • track: 16-Byte-ausgerichtete Speicheradresse der aufgenommenen Spur, 8 vorzeichenbehaftete 16-Bit-Samples.
  • bleed: Speicheradresse des zu entfernenden Übersprechens, 8 vorzeichenbehaftete 16-Bit-Samples (Ausrichtung nicht garantiert).
track  = {10000, -30000, 5000, 32767, 0,  200, -200, 15000}
bleed  = { 2000,   5000, 5000,    -5, 0,  300,  300,  5000}
result = { 8000, -32768,    0, 32767, 0, -100, -500, 10000}

Diese Funktion hat keinen Rückgabewert.

3. Pegel kombinieren

Über jedem Kanal sitzt eine Pegelanzeige, ein kleiner Balken, dessen Helligkeit zeigt, wie laut dieser Kanal ist. Die Frontplatte hat eine Hauptpegelanzeige, die so hell leuchten soll wie die beiden Kanäle zusammen. Du addierst die beiden Pegelblöcke, um die Hauptpegelanzeige anzusteuern.

Beachte, dass eine helle Pegelanzeige auf voller Helligkeit bleibt, statt wieder dunkel zu werden, daher ist die Summe sättigend.

Implementiere die Funktion combine_meters, die zwei Pegelblöcke Sample für Sample addiert. Diese Funktion nimmt folgende Argumente in dieser Reihenfolge entgegen:

  • result: 16-Byte-ausgerichtete Speicheradresse, an die die 16 kombinierten Werte geschrieben werden.
  • meter_a: 16-Byte-ausgerichtete Speicheradresse des Pegelblocks des ersten Kanals mit 16 gepackten vorzeichenlosen 8-Bit-Werten.
  • meter_b: 16-Byte-ausgerichtete Speicheradresse des Pegelblocks des zweiten Kanals mit 16 gepackten vorzeichenlosen 8-Bit-Werten.

Jeder Block besteht aus 16 vorzeichenlosen 8-Bit-Werten. Sie sollten addiert werden, wobei auf den vorzeichenlosen Bereich gesättigt wird.

meter_a = {200, 100,  50, 255, 0, 128, 10, 240, 1, 2, 3, 4, 5, 6, 7, 8}
meter_b = {100, 100, 200,   1, 0, 128, 10,  20, 1, 2, 3, 4, 5, 6, 7, 8}
result  = {255, 200, 250, 255, 0, 255, 20, 255, 2, 4, 6, 8, 10, 12, 14, 16}

Diese Funktion hat keinen Rückgabewert.

4. Eine Ausblendung anwenden

Der Song soll nicht abrupt enden, sondern am Ende ausblenden. Du hast einen Block von Verstärkungskoeffizienten, einen pro Sample, die von leiser zu noch leiser abnehmen.

Jeder Koeffizient ist ein Bruch, der über 65536 ausgedrückt wird. Wenn ein Sample zum Beispiel den Wert 16384 hat, drückt es tatsächlich 16384 / 65536 aus, also 1 / 4. Der größte nutzbare Wert, 32767, liegt knapp unter der Hälfte.

Da 65536 gleich 2^16 ist, ergibt die Multiplikation eines Samples mit seinem Koeffizienten und das Behalten der oberen Hälfte des Produkts sample * gain / 65536.

Implementiere die Funktion apply_fade, die jedes Sample mit seinem Koeffizienten skaliert. Diese Funktion nimmt folgende Argumente in dieser Reihenfolge entgegen:

  • result: 16-Byte-ausgerichtete Speicheradresse, an die alle skalierten Samples geschrieben werden.
  • track: 16-Byte-ausgerichtete Speicheradresse der Spur mit n vorzeichenbehafteten 16-Bit-Samples.
  • gains: 16-Byte-ausgerichtete Speicheradresse der Verstärkungskoeffizienten mit n vorzeichenbehafteten 16-Bit-Werten, jeweils im Bereich 0 bis 32767.
  • n: die Anzahl der Samples, die immer ungleich null und ein Vielfaches von 8 ist, als vorzeichenlose 64-Bit-Ganzzahl.

Beachte, dass die Spur ein Array ist, das n vorzeichenbehaftete 16-Bit-Samples enthält, wobei n nie null und immer ein Vielfaches von 8 ist. Für jedes Sample ist das skalierte Ergebnis die hohen 16 Bits des vorzeichenbehafteten Produkts: sample * gain.

track  = {20000, 20000, 20000, 20000, -20000, -20000, -20000, -20000}
gains  = {32767, 24576, 16384,  8192,  32767,  24576,  16384,   8192}
result = { 9999,  7500,  5000,  2500, -10000,  -7500,  -5000,  -2500}
Note

Die obere Hälfte zu behalten rundet in Richtung negative Unendlichkeit. Deshalb skaliert 20000 * 32767 zu 9999, während -20000 * 32767 zu -10000 statt zu -9999 skaliert.

Diese Funktion hat keinen Rückgabewert.

5. Eine Spur abschwächen

Der Schlagzeuger wurde viel zu laut aufgenommen. Um die ganze Spur herunterzuziehen, teilst du jedes Sample durch eine Konstante.

Implementiere die Funktion attenuate_track. Diese Funktion nimmt folgende Argumente in dieser Reihenfolge entgegen:

  • result: 16-Byte-ausgerichtete Speicheradresse, an die die Ergebnisse als vorzeichenbehaftete 32-Bit-Ganzzahlen geschrieben werden.
  • samples: 16-Byte-ausgerichtete Speicheradresse der Samples, n vorzeichenbehaftete 16-Bit-Werte.
  • divisor: 16-Byte-ausgerichtete Speicheradresse von 4 identischen Kopien des Divisors als 32-Bit-Ganzzahlen.
  • n ist die Anzahl der Samples, die immer ungleich null und ein Vielfaches von 4 ist, als vorzeichenlose 64-Bit-Ganzzahl.

Beachte, dass samples ein Array ist, das n vorzeichenbehaftete 16-Bit-Samples enthält, wobei n nie null und immer ein Vielfaches von 4 ist.

Der Divisor wird als 4 Kopien derselben 32-Bit-Ganzzahl bereitgestellt, bereits repliziert, damit er alle vier Lanes auf einmal füllen kann.

Ganzzahl-SIMD hat keine gepackte Division, daher wird die Arbeit in gepackten Gleitkommazahlen erledigt und dann zurück in gepackte 32-Bit-Ganzzahlen umgewandelt, um in result gespeichert zu werden. Wandle zurück, indem du gegen Null abschneidest, sodass ein Quotient von 2.3 zu 2 wird und -2.3 zu -2.

samples = {300, 150, 90, -120, 100, 50, -50, 7}
divisor = {3, 3, 3, 3}
result  = {100,  50, 30,  -40,  33, 16, -16, 2}

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 Mischpult 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.