Il codice scalare si affida ai flag impostati da varie istruzioni per saltare in risposta a una certa condizione. I valori packed, invece, rappresentano non uno ma molti valori in parallelo. Una singola condizione può fallire per una corsia e riuscire per un'altra.
Ecco perché il codice SIMD è senza rami per impostazione predefinita.
Invece di affidarsi ai flag, i confronti packed di solito producono una maschera nell'operando di destinazione.
Per ogni corsia, il confronto riempie l'intera corsia di uni quando è vera e di zeri quando è falsa.
Letta come intero con segno, una corsia vera è -1 e una corsia falsa è 0.
Questa maschera può poi essere composta con operazioni bit a bit per filtrare corsie specifiche.
Un cmp scalare è generico nel senso che viene usato per impostare vari flag contemporaneamente.
Un'altra istruzione può poi consumare quei flag per saltare o eseguire calcoli.
Tuttavia, poiché un confronto packed verifica una condizione e calcola una maschera allo stesso tempo, non è generico. Al confronto va indicata la condizione esatta da verificare.
Ci sono due modi per farlo:
eq per l'uguaglianza e gt per il maggiore di.
Le altre varianti si costruiscono componendo il risultato di uno di questi.A parte l'uso di un suffisso condizionale specifico nei confronti tra interi, la sintassi segue la stessa struttura che abbiamo già visto:
p + cmp + condizione + dimensione (b, w, d o q).cmp + p + dimensione (s o d).
La condizione viene passata in un immediato come operando aggiuntivo.Come accennato, per gli interi esistono solo confronti per l'uguaglianza e il maggiore di:
| istruzione | descrizione |
|---|---|
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq
|
uguaglianza per corsia |
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq
|
maggiore di con segno per corsia |
movdqa xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0
Per creare un confronto di minore, usa gt con gli operandi scambiati: a < b == b > a.
Nota che il confronto è con segno. Per eseguire un confronto senza segno, inverti il bit più alto di entrambi gli operandi. Lo si può fare con un XOR con una maschera in cui è impostato solo il bit più alto.
Due idiomi utili sono:
Per esempio:
pxor xmm4, xmm4 ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones
Tutti zeri e tutti uni sono maschere comuni per codificare rispettivamente «falso ovunque» e «vero ovunque».
Possono anche essere usate per rappresentare lo 0 packed o il -1 packed, che sono valori sentinella comuni.
Per esempio, il NUL che segna la fine di una stringa è uno 0.
Le corsie in virgola mobile usano una forma diversa: una sola istruzione, cmpps (e cmppd per corsie a 64 bit), con la condizione come immediato:
movaps xmm0, [rel readings]
cmpps xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]
NASM ha anche delle pseudo-istruzioni che corrispondono all'immediato corretto e sono più facili da ricordare.
In tutti i seguenti, la x in px può essere s (numeri in virgola mobile a 32 bit) o d (numeri in virgola mobile a 64 bit):
| pseudo-istruzione | immediato | confronto |
|---|---|---|
cmpeqpx |
0 | a == b |
cmpltpx |
1 | a < b |
cmplepx |
2 | a <= b |
cmpunordpx |
3 | a è NaN oppure b è NaN |
cmpneqpx |
4 | a != b |
cmpnltpx |
5 | a >= b |
cmpnlepx |
6 | a > b |
cmpordpx |
7 | né a né b è NaN |
Una maschera codifica il risultato di una condizione. Può poi essere usata per scegliere, corsia per corsia, tra due insiemi di valori in base a quella condizione. Prendiamo la corsia da un valore dove la maschera è vera, da un altro dove è falsa:
; 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
Nota che l'asimmetria di pandn qui ripaga: la maschera sta nella destinazione, viene negata e seleziona da b in una sola istruzione.
Questo schema è la forma packed della selezione senza rami.
Ogni corsia viene calcolata, e solo la maschera decide quale valore sopravvive, senza alcun jcc da nessuna parte.
Esistono istruzioni che eseguono direttamente quella stessa selezione, leggendo un bit per elemento da un registro di maschera. Si chiamano istruzioni blend:
| istruzione | elemento | origine della maschera |
|---|---|---|
pblendvb |
byte |
xmm0 implicito |
blendvps |
corsia a 32 bit |
xmm0 implicito |
blendvpd |
corsia a 64 bit |
xmm0 implicito |
Nota che la prima istruzione segue la sintassi degli interi, mentre le altre due seguono la sintassi dei numeri in virgola mobile. Tuttavia, poiché queste istruzioni selezionano semplicemente byte grezzi, ognuna di esse può essere usata sia con gli interi sia con i numeri in virgola mobile.
Per ogni elemento, il blend mantiene la destinazione quando il bit più alto dell'elemento di maschera corrispondente è a zero, e prende la sorgente quando è a uno.
Viene consultato solo quel bit più alto, cosa che una maschera di confronto soddisfa, dato che le sue corsie sono tutte uni o tutte zeri.
Il registro di maschera è sempre xmm0, che è implicito:
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
È anche possibile usare pblendvb per selezionare corsie da una maschera di confronto per qualsiasi altra dimensione.
Dato che tutti i byte in una corsia vera sono tutti uni, pblendvb li seleziona tutti.
Queste istruzioni aggiungono tutte una v dopo l'operazione eseguita (blend).
Questa v sta per variable, perché la selezione non è statica: dipende da un registro.
Ci sono anche varianti senza v, che selezionano in base a un immediato.
Seguono lo stesso schema, selezionando una corsia i se il bit i dell'immediato è impostato.
Per quanto potente, il codice SIMD manca di gran parte della flessibilità del codice scalare. In molte situazioni è necessario passare da un registro packed al mondo delle istruzioni scalari.
La famiglia di istruzioni movmsk fa da ponte tra i due mondi.
Queste istruzioni estraggono il bit più alto di ogni corsia in un registro di uso generale:
| istruzione | raccoglie | ampiezza del risultato |
|---|---|---|
pmovmskb |
bit più alto di ciascuno dei 16 byte | 16 bit |
movmskps |
bit più alto di ciascuno dei 4 dword | 4 bit |
movmskpd |
bit più alto di ciascuno dei 2 qword | 2 bit |
Se usata dopo un confronto, ogni bit impostato rappresenta una corsia «vera» e ogni bit azzerato una corsia «falsa».
Questo risultato può poi essere manipolato come al solito con istruzioni scalari.
Per esempio, un popcnt conta il numero di corrispondenze e tzcnt trova la prima.
Il registro di uso generale può essere largo 32 o 64 bit.
Esiste anche una variante packed dell'istruzione scalare test: ptest.
È simile alla sua controparte scalare in quanto esegue un'operazione AND tra due operandi, senza modificarli.
A differenza di test, ptest esegue anche un'operazione ANDN, negando il primo operando.
Quindi ptest può essere visto come una versione non distruttiva di pand e pandn che imposta i flag in base al risultato.
Più o meno allo stesso modo di queste due istruzioni, ptest tratta l'intero registro SIMD come un'unica corsia e quindi non accetta un prefisso di dimensione.
Se il risultato di un'operazione AND è 0, viene impostato il ZF, e se l'operazione ANDN dà come risultato 0, è il CF a essere impostato.
Questo significa che ptest può essere usato per verificare sia una maschera di tutti uni sia una di tutti zeri:
ptest su un registro con se stesso imposta il ZF solo se il registro è tutto zeri.
Questo imita l'idioma scalare comune di usare test su un registro con se stesso per verificare che sia 0.ptest su un registro con una maschera di tutti uni imposta il CF solo se il registro è tutto uni.
Inoltre imposta il ZF solo se il registro è tutto zeri, rendendo possibile verificare entrambe le maschere in una sola volta.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
Il risultato di un ptest può essere usato per saltare o in istruzioni senza rami come setcc o cmovcc, come al solito.
Gestisci la postazione di correzione di una scuola, dove valuti i risultati della classe un blocco alla volta.
Ogni blocco contiene 4 risultati e la postazione applica la stessa operazione a ogni risultato del blocco. Un punteggio è un numero in virgola mobile a 32 bit. Diversi passaggi lavorano con una maschera: un blocco di 4 corsie in cui ogni corsia è composta da tutti uno (un sì per quel risultato) oppure da tutti zero (un no).
Ci sono cinque attività. Gli operandi arrivano tramite indirizzi di memoria. Alcune attività scrivono la risposta a un indirizzo di risultato, mentre altre la restituiscono direttamente.
Tutti gli indirizzi di memoria in questo esercizio sono allineati a 16 byte.
In questo esercizio i calcoli dovrebbero essere eseguiti usando le istruzioni SIMD.
Il primo passaggio valuta ogni risultato rispetto a una soglia. Un risultato supera la soglia quando il suo punteggio è strettamente maggiore della soglia. Un punteggio minore o uguale alla soglia non la supera.
Implementa la funzione flag_above_threshold, che costruisce una maschera con una corsia di tutti uno per ogni punteggio sopra la sua soglia e una corsia di tutti zero altrimenti.
Questa funzione riceve come argomenti, in quest'ordine:
result: indirizzo di memoria di un buffer in cui vengono scritte le 4 corsie della maschera.scores: indirizzo di memoria dei punteggi, con 4 numeri in virgola mobile normali a 32 bit (mai NaN).thresholds: indirizzo di memoria della soglia di ogni corsia, con 4 numeri in virgola mobile normali a 32 bit (mai NaN).scores = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
Questa funzione non restituisce alcun valore.
Un report separato mette in evidenza i risultati perfetti, quelli che hanno raggiunto il punteggio massimo possibile.
Implementa la funzione flag_perfect, che costruisce una maschera con una corsia di tutti uno per ogni punteggio uguale al suo massimo e una corsia di tutti zero altrimenti.
Questa funzione riceve come argomenti, in quest'ordine:
result: indirizzo di memoria di un buffer in cui vengono scritte le 4 corsie della maschera.scores: indirizzo di memoria dei punteggi, con 4 numeri in virgola mobile normali a 32 bit (mai NaN).maxima: indirizzo di memoria del punteggio massimo di ogni corsia, con 4 numeri in virgola mobile normali a 32 bit (mai NaN).scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
Questa funzione non restituisce alcun valore.
Ogni punteggio ottiene un rango da 1 a 3:
50.0.Implementa la funzione assign_ranks, che scrive il rango di ogni punteggio.
Dovresti definire la soglia di superamento e i valori dei ranghi come costanti impacchettate in memoria. Le funzioni delle due attività precedenti possono essere riutilizzate: un punteggio è almeno di rango 2 quando è sopra la soglia, ed è di rango 3 quando è uguale al massimo.
Questa funzione riceve come argomenti, in quest'ordine:
result: indirizzo di memoria di un buffer in cui vengono scritti i 4 ranghi, ognuno un intero senza segno a 32 bit.scores: indirizzo di memoria dei punteggi, con 4 numeri in virgola mobile normali a 32 bit (mai NaN).maxima: indirizzo di memoria del punteggio massimo di ogni corsia, con 4 numeri in virgola mobile normali a 32 bit (mai NaN).scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}
Questa funzione non restituisce alcun valore.
Nel corso dell'anno, ogni studente accumula un rango totale. La postazione conta quanti ranghi, in tutta la coorte, scendono sotto una soglia di superamento, per pianificare quante lezioni extra tenere.
Implementa la funzione count_failures, che restituisce quanti ranghi, in tutti i blocchi, sono strettamente sotto la soglia di superamento.
La soglia è fornita come un blocco di 4 corsie identiche, così puoi caricarla una volta sola e riutilizzarla per ogni blocco.
Questa funzione riceve come argomenti, in quest'ordine:
ranks: indirizzo di memoria dei ranghi, un numero intero di blocchi da 4 corsie, dove ogni rango è un intero senza segno a 32 bit.block_count: il numero di blocchi da 4 corsie, sempre maggiore di 0.pass_threshold: indirizzo di memoria della soglia di superamento, con 4 interi identici a 32 bit.ranks = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count = 2
pass_threshold = {2, 2, 2, 2}
// => 3
Questa funzione restituisce il conteggio come intero con segno a 32 bit.
Prima che i registri vengano archiviati, la postazione controlla se la coorte è pulita: il controllo va a buon fine quando nessun risultato è fallito in alcun blocco.
Implementa la funzione all_passed, che restituisce 1 se tutti gli studenti hanno superato la prova, e 0 altrimenti.
Uno studente supera la prova quando la sua corsia corrispondente nell'array failing è composta da tutti zero.
Questa funzione riceve come argomenti, in quest'ordine:
failing: indirizzo di memoria delle maschere di fallimento, un numero intero di blocchi da 4 corsie, dove ogni corsia è di tutti uno o di tutti zero.block_count: il numero di blocchi da 4 corsie, sempre maggiore di 0.failing = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1
Questa funzione restituisce la risposta come intero con segno a 32 bit, cioè 1 oppure 0.
Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.