Keverőpult

Keverőpult

Tanulófeladat

Bevezetés

SIMD: egész számok

Egy korábbi fogalom bemutatta a csomagolt lebegőpontos értékeket: több számot, amelyek egyetlen xmm regiszterben vannak, és amelyeken sávonként, párhuzamosan hajtjuk végre a műveleteket. Ugyanezek az xmm regiszterek csomagolt egész számokat is tárolhatnak.

Szintaxis

A SIMD lebegőpontos modelljének nagy része változatlanul átvihető a csomagolt egészekre:

  • Egy 128 bites regiszter sávokra van felosztva
  • Az utasítások az azonos pozíciójú sávokon párhuzamosan működnek
  • A memóriaoperandusokra ugyanazok a 16 bájtos igazítási szabályok érvényesek

A szintaxis azonban némileg eltér:

  1. Egy p előtag, amely jelzi, hogy az utasítás csomagolt adatokon dolgozik.
  2. A végrehajtott művelet, ugyanazzal a névvel, mint a nem SIMD-beli (más néven skaláris) megfelelője (pl. add, mul stb.).
  3. Egy utótag, amely az egyes sávok méretét jelzi.

A csomagolt egészeknek négy sávméretük van, és mindegyiknek saját utótagja van:

sávszélesség bájt sávok 128 bitben utótag
bájt 1 16 b
word 2 8 w
dword 4 4 d
qword 8 2 q

Például:

utasítás jelentés
paddb csomagolt add, 8 bites sávok (16 sáv)
paddw csomagolt add, 16 bites sávok (8 sáv)
paddd csomagolt add, 32 bites sávok (4 sáv)
paddq csomagolt add, 64 bites sávok (2 sáv)

Egyes utasítások egy méretű sávokat vesznek bemenetként, de más méretűeket adnak kimenetként. Ugyanazt az általános konvenciót követik, de két méretutótaggal. Az első a bemeneti sáv méretét jelzi, a második a kimeneti sáv méretét:

utasítás jelentés
pmovsxwd csomagolt movsx, 16 bites sávokból 32 bites sávokba
pmuldq csomagolt mul, 32 bites sávokból 64 bites sávokba

Memóriamozgatás

Két, egész számra utaló nevű utasítás 128 bitet másol egy xmm regiszter és a memória között:

utasítás leírás
movdqa csomagolt egészek másolása igazított helyre vagy onnan
movdqu csomagolt egészek másolása igazítatlan helyre vagy onnan

Úgy viselkednek, mint a movaps és a movups: a movdqa hibát vált ki igazítatlan cím esetén, a movdqu viszont bármelyiket elfogadja. Mind a négy 128 bitet másol anélkül, hogy értelmezné őket. Az egész nevet viselő párt konvenció szerint használjuk egész adatokkal, nem kötelező jelleggel.

Note

A dq itt a double-qword rövidítése, azaz 128 bit (16 bájt).

Összeadás/kivonás

Az összeadás és a kivonás követi az elnevezési szabályt:

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

Nincs külön előjeles és előjel nélküli forma. A kettes komplemensben az összeadás és a kivonás ugyanazokat a biteket állítja elő, akár előjelesként, akár előjel nélküliként olvassuk a sávokat, így egyetlen utasítás szolgálja mindkettőt. Az értelmezés rajtad áll, pontosan úgy, mint a skaláris add és sub esetében.

Ezek az utasítások túlcsorduláskor átfordulnak, akárcsak a skaláris megfelelőik. Egy 8 bites sáv az értékeket 256 modulo szerint tárolja, így a 200 + 100 paddb művelete 300 - 256 = 44-et eredményez, eldobva a bele nem férő biteket.

Vedd észre, hogy azok a plusz bitek nem kerülnek át a következő sávba. Minden sávon külön-külön hajtjuk végre a műveletet, még akkor is, ha ugyanazt a regisztert használják.

Telítő összeadás/kivonás

Az egész SIMD olyan műveletet ad hozzá, amellyel a lebegőpontos SIMD nem rendelkezik: telítő összeadást és kivonást, amelyek a körbefordulás helyett leszorítanak. A sáv tartományát meghaladó eredmény a sáv által tárolható legnagyobb értékké válik; a tartomány alatti eredmény a legkisebbé.

A telítő formák a méretutótag elé illesztik az s (előjeles) vagy us (előjel nélküli) jelzést:

utasítás jelentés
paddsb add, telítő, előjeles 8 bites sávok
paddusb add, telítő, előjel nélküli 8 bites sávok
psubsw sub, telítő, előjeles 16 bites sávok
psubusw sub, telítő, előjel nélküli 16 bites sávok

A leszorítási tartomány az adott méretű és előjelű egész szám teljes ábrázolható tartománya. Egy bájt esetében:

  • Az előjel nélküli bájtok a [0, 255] tartományra szorulnak le: a 200 + 100 paddusb művelete 255-öt ad, az 5 - 10 psubusb művelete pedig 0-t.
  • Az előjeles bájtok a [-128, 127] tartományra szorulnak le: a 100 + 50 paddsb művelete 127-et ad.

A telítés akkor számít, amikor egy sáv korlátos mennyiséget tárol, például egy pixelcsatornát vagy egy hangmintát. A körbefordulás egy túl világos pixelt sötétre változtatna, a leszorítás viszont a maximális fényerőn tartja, ami a kívánt eredmény.

Note

Telítő összeadás és kivonás csak bájt és word sávokhoz létezik, dword vagy qword sávokhoz nem.

Szorzás

Két N bites érték összeszorzása 2N bites szorzatot eredményezhet, a célsáv viszont csak N bit széles. A SIMD műveletek ezt úgy oldják meg, hogy megadják, a szorzat melyik felét kell megtartani: az alsó N bitet vagy a felső N bitet.

A 16 bites sávokhoz három utasítást használunk:

utasítás jelentés
pmullw mul, 16 bites sávok, az egyes szorzatok alsó 16 bitjének megtartása
pmulhw mul, 16 bites sávok, a felső 16 bit megtartása, előjeles operandusok
pmulhuw mul, 16 bites sávok, a felső 16 bit megtartása, előjel nélküli operandusok

Egy szorzat alsó 16 bitje ugyanaz, akár előjelesként, akár előjel nélküliként olvassuk az operandusokat, ezért egyetlen pmullw létezik.

A felső 16 bit azonban az eredmény előjelétől függően változik. Ezért van a felső felet megtartó szorzásnak külön formája előjeles és előjel nélküli szorzásra:

  1. pmulhw, előjeles szorzáshoz.
  2. pmulhuw, egy plusz u-val, előjel nélküli szorzáshoz.

Figyeld meg a szintaxist:

  1. Egy p, a csomagolt egészre utalva.
  2. A végrehajtott művelet, mul.
  3. Egy h, amely jelzi, hogy az eredmény felső („high”) bitjeit választjuk ki.
  4. Egy opcionális u, ha az eredményt előjel nélküliként kell értelmezni (azaz nincs előjel-kiterjesztve).
  5. Végül a w méretutótag, amely jelzi, hogy word (16 bites) műveletről van szó.

A word sávokra vonatkozó csomagolt szorzás pontosan követi a fenti szabályt. A dword (32 bites) szorzás is követi a szabályt, de csak az alsó felet kiválasztó változat létezik: pmulld.

Nincs olyan változata a dword szorzásnak, amely a felső biteket választaná ki. Vannak azonban olyan változatok, amelyek kiszélesítik a szorzást, és a páros indexű sávok (azaz a 0. és 2. pozíciójú sávok) teljes 64 bites szorzatát tárolják:

utasítás jelentés
pmuludq a páros indexű 32 bites elemek szorzása előjel nélküliként 2 teljes 64 bites szorzattá
pmuldq a páros indexű 32 bites elemek szorzása előjelesként 2 teljes 64 bites szorzattá

Figyeld meg, hogy a szintaxis a dq-t használja, amit előjel nélküli szorzás esetén esetleg egy u előz meg. Ez azért van, mert az utasítások dword sávokat fogadnak és qword sávokat adnak ki.

Osztás

Nincs csomagolt egész osztás. Az ezt igénylő kód az értékeket lebegőpontossá alakítja, elosztja, majd visszaalakítja.

Egészek kiszélesítése

Léteznek csomagolt megfelelői a movsx és movzx utasításnak. Ugyanazt a szintaxist követik, amelyet azoknál az utasításoknál említettünk, amelyek bemenetének sávmérete eltér a kimenetétől:

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

Vedd észre, hogy a sávok számát a nagyobb (kimeneti) szélesség határozza meg. Az utasítás ennyi sávot olvas be a forrás alsó részéből. Ez ugyanaz a szemantika, amelyet már láttunk a csomagolt cvt utasításoknál az egyszeres és kétszeres pontosságú lebegőpontosok között.

Átváltás egészek és lebegőpontos számok között

Léteznek utasítások 32 bites előjeles egészek és lebegőpontos sávok közötti átváltásra. A cvt és cvtt utasítások szokásos szintaxisát követik, de az si helyett a csomagolt egészt a dq jelöli:

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

Azok az utasítások, amelyek a pi-t használják a csomagolt egészek jelölésére, a régi mmx regiszterekbe írnak, és az x86-64-en gyakorlatilag elavultnak számítanak. Részesítsd előnyben a dq formákat, amelyek xmm regisztereket használnak.

Ugyanazok a megjegyzések érvényesek itt is, amelyeket a lebegőpontos számok és egészek közötti skaláris átváltásnál tettünk. A lebegőpontos számokat egy MXCSR nevű speciális regiszter szerint kerekítjük, amelynek módját nem feltételezheted a függvénybe való belépéskor. Létezik egy cvtt változat (egy plusz t-vel), amely mindig csonkolja az eredményt.

Az is lehetséges, hogy a round segítségével a csomagolt lebegőpontos értékeket ismert állapotba hozzuk az átváltás előtt. A kerekítés vezérlőértéke ugyanaz, mint a skaláris round esetében, és a szintaxis is ugyanaz a csomagolt lebegőpontos értékeknél:

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

Utasítások

Egy hangstúdió kisméretű keverőpultjának digitális magját írod.

A hang minták folyamaként érkezik. Minden minta egy szám, amely egy hangszórómembrán pillanatnyi helyzetét írja le. A keverőpult egyszerre egy egész mintablokkot dolgoz fel, és mindegyikre ugyanazt a műveletet alkalmazza.

Öt részfeladat vár rád, és mindegyik a memóriában tárolt mintablokkon dolgozik. Az operandusokat memóriacímeken keresztül kapod, az eredményt pedig egy eredménycímre írod.

Note

Ebben a feladatban a számításokat SIMD utasításokkal kell elvégezni, nem skaláris műveletekkel.

1. Két sáv keverése

A stúdió néha külön sávokra veszi fel a hangszereket. Ahhoz, hogy ugyanazokon a hangszórókon szólaljanak meg, a két sávot összeadod eggyé.

Valósítsd meg a mix_tracks függvényt, amely mintánként összeadja a két sávot. Minden sáv egy 8 előjeles 16 bites mintából álló blokk. Az előjeles 16 bites tartományt túllépő összegnek telítődnie kell a körbefordulás helyett.

A függvény argumentumai a következő sorrendben:

  • result: a kevert 8 minta kiírására szolgáló puffer 16 bájtos határra igazított memóriacíme.
  • track_a: az első sáv 16 bájtos határra igazított memóriacíme, 8 előjeles 16 bites mintával.
  • track_b: a második sáv 16 bájtos határra igazított memóriacíme, 8 előjeles 16 bites mintával.
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}

A függvénynek nincs visszatérési értéke.

2. Az áthallás eltávolítása

Amikor két mikrofon egyszerre vesz fel, mindegyik gyengén felveszi a másik forrásának egy másolatát. Ezt a nem kívánt másolatot áthallásnak nevezik. Miután megmérted ezt az áthallást, vissza kell vonnod, hogy megtisztítsd a sávot.

Valósítsd meg a remove_bleed függvényt, amely mintánként kivonja az áthallásblokkot a sávblokkból. Mindkét blokk 8 előjeles 16 bites minta, és a kivonásnak telítődnie kell.

A függvény argumentumai a következő sorrendben:

  • result: memóriacím, ahová a megtisztított 8 minta kerül (az igazítás nem garantált).
  • track: a felvett sáv 16 bájtos határra igazított memóriacíme, 8 előjeles 16 bites minta.
  • bleed: az eltávolítandó áthallás memóriacíme, 8 előjeles 16 bites minta (az igazítás nem garantált).
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}

A függvénynek nincs visszatérési értéke.

3. A mérők összevonása

Minden csatorna fölött egy szintmérő található, egy kis sáv, amelynek fényessége mutatja, milyen hangos az adott csatorna. Az előlapon egyetlen fő mérő van, amelynek olyan fényesen kell világítania, mint a két csatorna együttvéve. Ezt a két mérőblokk összeadásával vezérled.

Figyeld meg, hogy a fényes mérő a maximális fényerőn marad ahelyett, hogy visszafordulna a sötétbe, így az összeg telítődik.

Valósítsd meg a combine_meters függvényt, amely mintánként összeadja a két mérőblokkot. A függvény argumentumai a következő sorrendben:

  • result: 16 bájtos határra igazított memóriacím, ahová a 16 összevont érték kerül.
  • meter_a: az első csatorna mérőblokkjának 16 bájtos határra igazított memóriacíme, 16 csomagolt előjel nélküli 8 bites értékkel.
  • meter_b: a második csatorna mérőblokkjának 16 bájtos határra igazított memóriacíme, 16 csomagolt előjel nélküli 8 bites értékkel.

Mindkét blokk 16 előjel nélküli 8 bites érték. Ezeket össze kell adni, az előjel nélküli tartományra telítve.

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}

A függvénynek nincs visszatérési értéke.

4. Elhalványítás alkalmazása

A dalnak nem szabad hirtelen véget érnie, hanem a végén el kell halványulnia. Van egy blokk erősítési együtthatód, mintánként egy, amelyek egyre halkabbra, majd még halkabbra csökkennek.

Minden együttható egy 65536-ra vonatkoztatott tört. Például ha egy minta értéke 16384, az valójában 16384 / 65536-ot fejez ki, ami 1 / 4. A legnagyobb használható érték, a 32767, éppen a fél alatt van.

Mivel a 65536 éppen 2^16, ha egy mintát megszorzod az együtthatójával, és megtartod a szorzat felső felét, az sample * gain / 65536-ot kapod.

Valósítsd meg az apply_fade függvényt, amely minden mintát a hozzá tartozó együtthatóval skáláz. A függvény argumentumai a következő sorrendben:

  • result: 16 bájtos határra igazított memóriacím, ahová az összes skálázott minta kerül.
  • track: a sáv 16 bájtos határra igazított memóriacíme, n előjeles 16 bites mintával.
  • gains: az erősítési együtthatók 16 bájtos határra igazított memóriacíme, n előjeles 16 bites értékkel, mindegyik a 0 és 32767 közötti tartományban.
  • n: a minták száma, amely mindig nem nulla és a 8 többszöröse, 64 bites előjel nélküli egészként.

Figyeld meg, hogy a sáv egy tömb, amely n előjeles 16 bites mintát tartalmaz, ahol n soha nem nulla, és mindig a 8 többszöröse. Minden minta esetén a skálázott eredmény az előjeles szorzat felső 16 bitje: 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

A felső fél megtartása a negatív végtelen felé kerekít. Ezért skálázódik a 20000 * 32767 9999-re, míg a -20000 * 32767 -10000-re -9999 helyett.

A függvénynek nincs visszatérési értéke.

5. Egy sáv csillapítása

A dobost jóval túl hangosan vették fel. Hogy az egész sávot lehalkítsd, minden mintát elosztasz egy konstanssal.

Valósítsd meg az attenuate_track függvényt. A függvény argumentumai a következő sorrendben:

  • result: 16 bájtos határra igazított memóriacím, ahová az eredmények 32 bites előjeles egészként kerülnek.
  • samples: a minták 16 bájtos határra igazított memóriacíme, n előjeles 16 bites érték.
  • divisor: az osztó 4 azonos másolatának 16 bájtos határra igazított memóriacíme, 32 bites egészként.
  • n a minták száma, amely mindig nem nulla és a 4 többszöröse, 64 bites előjel nélküli egészként.

Figyeld meg, hogy a samples egy tömb, amely n előjeles 16 bites mintát tartalmaz, ahol n soha nem nulla, és mindig a 4 többszöröse.

Az osztót ugyanannak a 32 bites egésznek a 4 másolataként kapod, már replikálva, hogy egyszerre kitölthesse mind a négy sávot.

Az egész számos SIMD-nek nincs csomagolt osztása, ezért a művelet csomagolt lebegőpontos számokkal történik, majd visszaalakítjuk csomagolt 32 bites egészekké, hogy a result-ba kerüljenek. A visszaalakítás nulla felé csonkítással történik, így a 2.3 hányadosból 2 lesz, a -2.3-ból pedig -2.

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

A függvénynek nincs visszatérési értéke.

Szerkesztés GitHubon A hivatkozás új ablakban vagy lapon nyílik meg
x86-64 Assembly Exercism

Készen állsz elkezdeni a(z) Keverőpult feladatot?

Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) x86-64 Assembly nyelvet 22 fogalom130 feladat segítségével, valódi emberi mentorálással, mindez ingyen.