Un concetto precedente ha introdotto i valori in virgola mobile impacchettati: più numeri contenuti in un unico registro xmm, su cui si opera corsia per corsia e in parallelo.
Gli stessi registri xmm possono contenere anche numeri interi impacchettati.
Gran parte del modello SIMD per la virgola mobile si trasferisce invariato ai numeri interi impacchettati:
La sintassi, però, è leggermente diversa:
p che indica che l'istruzione opera su dati impacchettati.add, mul, ecc.).I numeri interi impacchettati hanno quattro dimensioni di corsia, e ognuna ha il proprio suffisso:
| larghezza della corsia | byte | corsie in 128 bit | suffisso |
|---|---|---|---|
| byte | 1 | 16 | b |
| word | 2 | 8 | w |
| dword | 4 | 4 | d |
| qword | 8 | 2 | q |
Per esempio:
| istruzione | significato |
|---|---|
paddb |
add impacchettata, corsie a 8 bit (16 corsie) |
paddw |
add impacchettata, corsie a 16 bit (8 corsie) |
paddd |
add impacchettata, corsie a 32 bit (4 corsie) |
paddq |
add impacchettata, corsie a 64 bit (2 corsie) |
Alcune istruzioni accettano in input corsie di una dimensione, ma producono in output corsie di un'altra dimensione. Seguono la stessa convenzione generale, ma con due suffissi di dimensione. Il primo indica la dimensione della corsia di input, il secondo indica la dimensione della corsia di output:
| istruzione | significato |
|---|---|
pmovsxwd |
movsx impacchettata, da corsie a 16 bit a corsie a 32 bit |
pmuldq |
mul impacchettata, da corsie a 32 bit a corsie a 64 bit |
Due istruzioni con nome da interi copiano 128 bit tra un registro xmm e la memoria:
| istruzione | descrizione |
|---|---|
movdqa |
copia numeri interi impacchettati da o verso una posizione allineata |
movdqu |
copia numeri interi impacchettati da o verso una posizione non allineata |
Si comportano come movaps e movups: movdqa genera un fault su un indirizzo non allineato, mentre movdqu accetta qualsiasi indirizzo.
Tutte e quattro copiano 128 bit senza interpretarli.
La coppia con nome da interi si usa con dati interi per convenzione, non per obbligo.
Qui dq sta per double-qword, cioè 128 bit (16 byte).
L'addizione e la sottrazione seguono la regola di denominazione:
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
Non esiste una forma separata con segno e senza segno.
Nel complemento a due, l'addizione e la sottrazione producono gli stessi bit sia che le corsie vengano lette come con segno sia come senza segno, quindi un'unica istruzione serve entrambi i casi.
L'interpretazione spetta a chi scrive il codice, esattamente come con add e sub scalari.
Queste istruzioni fanno il wrap-around in caso di overflow, come le loro controparti scalari.
Una corsia a 8 bit contiene valori modulo 256, quindi una paddb di 200 + 100 produce 300 - 256 = 44, scartando i bit che non ci stanno.
Nota che quei bit in eccesso non si propagano alla corsia successiva. Ogni corsia viene elaborata separatamente dalle altre, anche se condividono lo stesso registro.
Il SIMD su interi aggiunge un'operazione che il SIMD in virgola mobile non ha: l'addizione e la sottrazione con saturazione, che limitano il risultato invece di fare il wrap-around. Un risultato superiore all'intervallo della corsia diventa il valore più grande che la corsia può contenere; un risultato inferiore all'intervallo diventa il più piccolo.
Le forme con saturazione inseriscono s (con segno) o us (senza segno) prima del suffisso di dimensione:
| istruzione | significato |
|---|---|
paddsb |
add, con saturazione, corsie a 8 bit con segno |
paddusb |
add, con saturazione, corsie a 8 bit senza segno |
psubsw |
sub, con saturazione, corsie a 16 bit con segno |
psubusw |
sub, con saturazione, corsie a 16 bit senza segno |
L'intervallo di limitazione è l'intero intervallo rappresentabile per un numero intero della dimensione e del segno corrispondenti. Per un byte:
[0, 255]: una paddusb di 200 + 100 dà 255, ed una psubusb di 5 - 10 dà 0.[-128, 127]: una paddsb di 100 + 50 dà 127.La saturazione è importante quando una corsia contiene una quantità limitata, come il canale di un pixel o un campione audio. Il wrap-around renderebbe scuro un pixel troppo luminoso, mentre la limitazione lo mantiene alla luminosità massima, che è il risultato che vuoi.
L'addizione e la sottrazione con saturazione esistono solo per le corsie byte e word, non per quelle dword o qword.
Moltiplicare due valori da N bit può produrre un prodotto da 2N bit, ma la corsia di destinazione è larga solo N bit. Le operazioni SIMD risolvono il problema specificando quale metà del prodotto conservare, i bit bassi (N bit) oppure i bit alti (N bit).
Per le corsie a 16 bit si usano tre istruzioni:
| istruzione | significato |
|---|---|
pmullw |
mul, corsie a 16 bit, conserva i 16 bit bassi di ogni prodotto |
pmulhw |
mul, corsie a 16 bit, conserva i 16 bit alti, operandi con segno |
pmulhuw |
mul, corsie a 16 bit, conserva i 16 bit alti, operandi senza segno |
I 16 bit bassi di un prodotto sono gli stessi sia che gli operandi vengano letti come con segno sia come senza segno, quindi esiste un'unica pmullw.
I 16 bit alti, invece, variano a seconda che il risultato sia con segno o senza segno. Ecco perché la moltiplicazione della metà alta ha forme separate per la moltiplicazione con segno e senza segno:
pmulhw, per la moltiplicazione con segno.pmulhuw, con una u in più, per la moltiplicazione senza segno.Nota la sintassi:
p, per indicare un numero intero impacchettato.mul.h, per indicare che vengono selezionati i bit alti («high») del risultato.u opzionale se il risultato va interpretato come senza segno (cioè non è esteso con il segno).w, per indicare che è un'operazione su word (16 bit).La moltiplicazione impacchettata per le word segue esattamente la regola qui sopra.
Anche la moltiplicazione dword (32 bit) segue la regola, ma esiste solo la variante che seleziona la metà bassa: pmulld.
Non esiste una variante della moltiplicazione dword che selezioni i bit alti. Esistono però varianti che ampliano la moltiplicazione, memorizzando il prodotto completo a 64 bit delle corsie con indice pari (cioè le corsie in posizione 0 e 2):
| istruzione | significato |
|---|---|
pmuludq |
moltiplica gli elementi a 32 bit con indice pari, senza segno, in 2 prodotti completi da 64 bit |
pmuldq |
moltiplica gli elementi a 32 bit con indice pari, con segno, in 2 prodotti completi da 64 bit |
Nota che la sintassi usa dq, eventualmente preceduto da una u per la moltiplicazione senza segno.
Questo perché le istruzioni accettano corsie dword e producono corsie qword.
Non esiste una divisione tra numeri interi impacchettati. Il codice che ne ha bisogno converte i valori in virgola mobile, divide e riconverte.
Esistono equivalenti impacchettati di movsx e movzx.
Seguono la stessa sintassi che abbiamo citato per le istruzioni che accettano input con una dimensione di corsia diversa da quella dell'output:
pmovsxwd xmm0, xmm1 ; 4 words -> 4 dwords, sign-extended
pmovzxbw xmm0, xmm1 ; 8 bytes -> 8 words, zero-extended
Nota che il numero di corsie è determinato dalla larghezza maggiore (quella dell'output).
L'istruzione legge quel numero di corsie dalla parte bassa della sorgente.
È la stessa semantica che abbiamo già visto per le istruzioni cvt impacchettate tra numeri in virgola mobile a precisione singola e doppia.
Esistono istruzioni per convertire tra numeri interi con segno a 32 bit e corsie in virgola mobile.
Seguono la solita sintassi delle istruzioni cvt e cvtt, ma al posto di si un numero intero impacchettato è rappresentato da dq:
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
Le istruzioni che usano pi per rappresentare i numeri interi impacchettati scrivono su registri mmx legacy e sono di fatto deprecate su x86-64.
Preferisci le forme con dq, che usano i registri xmm.
Le stesse osservazioni fatte per la conversione scalare tra numeri in virgola mobile ed interi valgono anche qui.
I numeri in virgola mobile vengono arrotondati in base a uno speciale registro chiamato MXCSR, di cui non puoi dare per scontato il modo all'ingresso di una funzione.
Esiste una variante cvtt (con una t in più) che tronca sempre il risultato.
È anche possibile usare round per portare i valori in virgola mobile impacchettati in uno stato noto prima di convertirli.
Il valore di controllo di round è lo stesso della round scalare e la sintassi è la stessa anche per i valori in virgola mobile impacchettati:
roundps xmm0, xmm1, 1 ; xmm0 = floor(xmm1), packed 32-bit floats
roundpd xmm2, xmm3, 2 ; xmm2 = ceil(xmm3), packed 64-bit floats
Stai scrivendo il cuore digitale di un piccolo mixer in uno studio di registrazione.
Il suono arriva sotto forma di un flusso di campioni. Ogni campione è un numero che descrive la posizione del cono di un altoparlante in un preciso istante. Un mixer elabora un intero blocco di campioni alla volta, applicando la stessa operazione a ciascuno.
Hai cinque attività, ognuna delle quali opera su un blocco di campioni tenuto in memoria. Ricevi gli operandi tramite indirizzi di memoria e scrivi il risultato tramite un indirizzo di risultato.
In questo esercizio i calcoli vanno eseguiti usando istruzioni SIMD, non operazioni scalari.
A volte lo studio registra gli strumenti su tracce diverse. Per riprodurli dagli stessi altoparlanti, sommi le due tracce in una sola.
Implementa la funzione mix_tracks, che somma due tracce campione per campione.
Ogni traccia è un blocco di 8 campioni con segno a 16 bit.
Una somma che esce dall'intervallo a 16 bit con segno deve saturare, non riportare il valore.
Questa funzione accetta come argomenti, in quest'ordine:
result: indirizzo di memoria allineato a 16 byte per un buffer in cui vengono scritti gli 8 campioni mixati.track_a: indirizzo di memoria allineato a 16 byte della prima traccia, con 8 campioni con segno a 16 bit.track_b: indirizzo di memoria allineato a 16 byte della seconda traccia, con 8 campioni con segno a 16 bit.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}
Questa funzione non restituisce alcun valore.
Quando due microfoni registrano contemporaneamente, ognuno capta una copia debole della sorgente dell'altro. Questa copia indesiderata si chiama bleed. Una volta misurato questo bleed, va sottratto per ripulire la traccia.
Implementa la funzione remove_bleed, che sottrae il blocco di bleed dal blocco della traccia, campione per campione.
Ogni blocco è composto da 8 campioni con segno a 16 bit e la sottrazione deve saturare.
Questa funzione accetta come argomenti, in quest'ordine:
result: indirizzo di memoria in cui vengono scritti gli 8 campioni ripuliti (allineamento non garantito).track: indirizzo di memoria allineato a 16 byte della traccia registrata, 8 campioni con segno a 16 bit.bleed: indirizzo di memoria del bleed da rimuovere, 8 campioni con segno a 16 bit (allineamento non garantito).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}
Questa funzione non restituisce alcun valore.
Sopra ogni canale c'è un indicatore di livello, una piccola barra la cui luminosità mostra quanto è forte quel canale. Il pannello frontale ha un indicatore master che deve brillare quanto la combinazione dei due canali. Sommi i due blocchi degli indicatori per pilotarlo.
Nota che un indicatore luminoso resta alla massima luminosità invece di tornare al buio, quindi la somma satura.
Implementa la funzione combine_meters, che somma due blocchi di indicatori campione per campione.
Questa funzione accetta come argomenti, in quest'ordine:
result: indirizzo di memoria allineato a 16 byte in cui vengono scritti i 16 valori combinati.meter_a: indirizzo di memoria allineato a 16 byte del blocco dell'indicatore del primo canale, con 16 valori senza segno a 8 bit impacchettati.meter_b: indirizzo di memoria allineato a 16 byte del blocco dell'indicatore del secondo canale, con 16 valori senza segno a 8 bit impacchettati.Ogni blocco è composto da 16 valori senza segno a 8 bit. Vanno sommati tra loro, saturando all'intervallo senza segno.
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}
Questa funzione non restituisce alcun valore.
La canzone non deve interrompersi bruscamente, ma deve sfumare alla fine. Hai un blocco di coefficienti di guadagno, uno per campione, che diminuiscono sempre di più.
Ogni coefficiente è una frazione espressa su 65536.
Per esempio, se un campione ha un valore di 16384, in realtà esprime 16384 / 65536, cioè 1 / 4.
Il valore massimo utilizzabile, 32767, è appena sotto la metà.
Dato che 65536 è 2^16, moltiplicare un campione per il suo coefficiente e tenere la metà alta del prodotto dà sample * gain / 65536.
Implementa la funzione apply_fade, che scala ogni campione in base al suo coefficiente.
Questa funzione accetta come argomenti, in quest'ordine:
result: indirizzo di memoria allineato a 16 byte in cui vengono scritti tutti i campioni scalati.track: indirizzo di memoria allineato a 16 byte della traccia, con n campioni con segno a 16 bit.gains: indirizzo di memoria allineato a 16 byte dei coefficienti di guadagno, con n valori con segno a 16 bit, ciascuno nell'intervallo da 0 a 32767.n: il numero di campioni, che è sempre diverso da zero e multiplo di 8, come intero senza segno a 64 bit.Nota che la traccia è un array che contiene n campioni con segno a 16 bit, dove n non è mai zero ed è sempre un multiplo di 8.
Per ogni campione, il risultato scalato è costituito dai 16 bit alti del prodotto con segno: 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}
Tenere la metà alta arrotonda verso meno infinito.
Ecco perché 20000 * 32767 scala a 9999, mentre -20000 * 32767 scala a -10000 invece che a -9999.
Questa funzione non restituisce alcun valore.
Il batterista è stato registrato troppo forte. Per abbassare l'intera traccia, dividi ogni campione per una costante.
Implementa la funzione attenuate_track.
Questa funzione accetta come argomenti, in quest'ordine:
result: indirizzo di memoria allineato a 16 byte in cui vengono scritti i risultati, come interi con segno a 32 bit.samples: indirizzo di memoria allineato a 16 byte dei campioni, n valori con segno a 16 bit.divisor: indirizzo di memoria allineato a 16 byte di 4 copie identiche del divisore, come interi a 32 bit.n è il numero di campioni, che è sempre diverso da zero e multiplo di 4, come intero senza segno a 64 bit.Nota che samples è un array che contiene n campioni con segno a 16 bit, dove n non è mai zero ed è sempre un multiplo di 4.
Il divisore viene fornito come 4 copie dello stesso intero a 32 bit, già replicate in modo che possa riempire tutte e quattro le lane in una volta.
Il SIMD intero non ha una divisione impacchettata, quindi il lavoro viene svolto in virgola mobile impacchettata e poi riconvertito in interi a 32 bit impacchettati da memorizzare in result.
Riconverti troncando verso lo zero, quindi un quoziente di 2.3 diventa 2 e -2.3 diventa -2.
samples = {300, 150, 90, -120, 100, 50, -50, 7}
divisor = {3, 3, 3, 3}
result = {100, 50, 30, -40, 33, 16, -16, 2}
Questa funzione non restituisce alcun valore.
Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.