Vettori SoA su SPU


8

Ho letto molto sui vantaggi dell'organizzazione dei dati in "Struct of Arrays" (SoA) anziché nel tipico "Array of Structs" (AoS) per ottenere un throughput migliore quando si utilizzano le istruzioni SIMD . Mentre il "perché" ha un senso totale per me, non sono sicuro di quanto fare quando si lavora con cose come i vettori.

I vettori stessi possono essere considerati come una struttura di una matrice di dati (dimensioni fisse), quindi è possibile convertire una matrice di questi in una struttura di matrici X, Y e Z. Attraverso questo, puoi lavorare su 4 vettori contemporaneamente anziché uno alla volta.

Ora, per il motivo specifico, sto pubblicando questo su GameDev:

Ha senso lavorare con i vettori sulla SPU? Più specificamente, ha senso eseguire il DMA su più array solo per un singolo vettore? O sarebbe meglio attenersi al DMAing della matrice di vettori e srotolarli nei diversi componenti con cui lavorare?

Potrei vedere il vantaggio di eliminare lo srotolamento (se lo facessi "AoS"), ma sembra che potresti esaurire rapidamente i canali DMA se prendessi questa strada e lavorassi con più set di vettori contemporaneamente.

(Nota: nessuna esperienza professionale con Cell ancora, ma sono stato in giro in OtherOS per un po ')

Risposte:


5

Un approccio consiste nell'utilizzare un approccio AoSoA (leggi: Array of Struct of Array) che è un ibrido di AoS e SoA. L'idea è quella di archiviare N strutture di valore di dati in un blocco contiguo in forma SoA, quindi le successive N strutture in valore SoA.

Il modulo AoS per 16 vettori (etichettato 0,1,2 ... F), ruotato alla granularità di 4 strutture è:

000111222333444555666777888999AAABBBCCCDDDEEEFFF
XYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZXYZ

per SoA, questo è:

0123456789ABCDEF
xXXXXXXXXXXXXXXX

0123456789ABCDEF
YYYYYYYYYYYYYYYY

0123456789ABCDEF
ZZZZZZZZZZZZZZZZ

per AoSoA, questo diventa:

01230123012345674567456789AB89AB89ABCDEFCDEFCDEF
XXXXYYYYZZZZXXXXYYYYZZZZXXXXYYYYZZZZXXXXYYYYZZZZ

L'approccio AoSoA ha i seguenti vantaggi di AoS:

  • È necessario un solo trasferimento DMA per trasferire un blocco di strutture nella memoria locale SPU.
  • le strutture hanno ancora la possibilità che tutti i dati vengano inseriti in una cache.
  • Il prefetch dei blocchi è ancora molto semplice.

L'approccio AoSoA ha anche questi vantaggi del modulo SoA:

  • È possibile caricare i dati dalla memoria locale SPU direttamente nei registri vettoriali a 128 bit senza scorrere i dati.
  • Puoi ancora operare su 4 strutture contemporaneamente.
  • Puoi utilizzare appieno la SIMD'ness del tuo processore vettoriale se non vi sono ramificazioni di base (ad es. Corsie non utilizzate nell'aritmetica vettoriale)

L'approccio AoSoA presenta ancora alcuni di questi svantaggi del modulo SoA:

  • la gestione degli oggetti deve essere fatta con granularità frizzante.
  • le scritture ad accesso casuale di una struttura completa ora devono toccare la memoria sparsa.
  • (questi possono rivelarsi non problematici a seconda di come organizzi / gestisci le tue strutture e la loro durata)

A proposito, questi concetti di AoSoA si applicano molto bene a SSE / AVX / LRBni, nonché alle GPU che possono essere paragonate a processori SIMD molto ampi, ad es. 32/48/64 di larghezza a seconda del fornitore / architettura.


Non vedo come questo offra alcun vantaggio rispetto al non impacchettarli per componente a meno che tu non stia imballando dati non vettoriali che usi effettivamente come float - anche se vedo che il tuo AoS esclude W, che non sembrerebbe molto facile da usare per l'accesso alla memoria, I suppongo che in quel caso ci sia una vittoria. Si noti inoltre che le SPU non hanno linee di cache tranne che per comunicare con la memoria principale.
Kaj,

2
1. Come per tutte le cose, il chilometraggio può variare a seconda dei dati / algoritmo / processore esatti. In casi vincolati dal registro, può essere utile evitare la necessità di 4 registri temporali prima di poter mescolare tutti i campi X nello stesso registro. Ma ancora una volta, YMMV. 2. La mia risposta è stata più generale perché i concetti si trasferiscono bene nel campo della programmazione parallela dei dati; le considerazioni sulle linee di cache sono più pertinenti per GPU / SSE ma ho sentito che dovrei menzionarle tutte uguali :)
jpaver

1
Abbastanza giusto, rimango illuminato e imparerò a criticare più sottilmente! Grazie per aver condiviso la tua visione: o)
Kaj,

3

Le SPU sono in realtà un caso speciale interessante quando si tratta di vettorializzare il codice. Le istruzioni sono divise in famiglie "aritmetiche" e "carico / deposito" e le due famiglie funzionano su condotte separate. L'SPU può emettere uno di ciascun tipo per ciclo.

Il codice matematico è ovviamente fortemente vincolato dalle istruzioni matematiche, quindi di solito i loop di matematica su SPU avranno un sacco di cicli aperti sul tubo di carico / deposito. Dato che si verificano shuffle sul tubo di carico / deposito, spesso si hanno sufficienti istruzioni di carico / deposito gratuite per far girare la forma xyzxyzxyzxyz nella forma xxxxyyyyzzzz senza alcun sovraccarico.

Questa tecnica è in uso almeno su Naughty Dog - vedi le loro presentazioni di assemblaggio SPU ( parte 1 e parte 2 ) per i dettagli.

Sfortunatamente il compilatore spesso non è abbastanza intelligente da farlo automaticamente - se decidi di seguire questa strada dovrai scrivere tu stesso l'assemblaggio o srotolare i tuoi loop usando intrinseci e controllare l'assemblatore per assicurarti che sia quello che vuoi. Quindi, se stai cercando di scrivere codice multipiattaforma generale che funziona bene su SPU, potresti voler andare con SoA o AoSoA (come suggerisce jpaver.)


Ah, dopo tutto siamo d'accordo: o) Swizzle sulla SPU se ne hai bisogno, abbastanza tempo per farlo lì.
Kaj,

1

Come con qualsiasi ottimizzazione, profilo! La leggibilità viene prima di tutto e dovrebbe essere sacrificata solo quando la profilazione identifica un particolare collo di bottiglia e hai esaurito tutte le opzioni per ottimizzare l'algoritmo di alto livello (il modo più veloce per fare il lavoro è non dover fare il lavoro!) Dovresti sempre riprofilare seguendo qualsiasi ottimizzazione di basso livello per confermare che hai davvero reso le cose più veloci piuttosto che il contrario, specialmente con condutture bizzarre come quelle di Cell.

Quali tecniche userete allora dipenderanno dai dettagli del collo di bottiglia. In generale, quando si lavora con tipi di vettore, un componente vettoriale che si ignora in un risultato rappresenta spreco di lavoro. Cambiare SoA / AoS non ha senso a meno che non ti consenta di fare un lavoro più utile riempiendo tali componenti inutilizzati (ad esempio un prodotto a punti sulla PPU della PS3 contro quattro prodotti a punti in parallelo nello stesso periodo di tempo). Per rispondere alla tua domanda, passare il tempo a mescolare i componenti solo per eseguire un'operazione su un singolo vettore mi sembra una pessimizzazione!

Il rovescio della medaglia sulle SPU è che la maggior parte del costo dei piccoli trasferimenti DMA è in fase di configurazione; qualsiasi cosa inferiore a 128 byte richiederà lo stesso numero di cicli per il trasferimento e qualsiasi cosa inferiore a un kilobyte solo pochi cicli in più. Quindi non preoccuparti di DMAing di più dati di quelli strettamente necessari; ridurre il numero di trasferimenti DMA sequenziali attivati ​​e svolgere attività mentre si stanno verificando trasferimenti DMA - e quindi sviluppare prologhi ed epiloghi di loop per formare pipeline di software - è la chiave per buone prestazioni SPU ed è più facile gestire casi angolari recuperando dati extra / scartando i risultati parzialmente calcolati che saltare attraverso i cerchi per cercare di organizzare l'esatta quantità di dati necessari per essere letti ed elaborati.


Se finisci per disimballarli, secondo l'approccio AOSAO, almeno attira più vettori contemporaneamente. Inoltre, vorrai inserire un batch e, durante l'elaborazione, estrarre il batch successivo. Durante l'invio del primo batch, si elabora il secondo e si inserisce il terzo. In questo modo nascondi quanta più latenza puoi.
Kaj,

0

No, non avrebbe molto senso in generale poiché la maggior parte dei codici operativi vettoriali operano su un vettore nel suo insieme e non su componenti separati. Quindi puoi già moltiplicare un vettore in 1 istruzione, mentre con la suddivisione dei componenti separati passeresti 4 istruzioni su di esso. Quindi, dal momento che praticamente fai molte operazioni in generale su una struttura, è meglio imballarle in un array, ma non fai quasi mai cose solo su un componente di un vettore, o selvaggiamente diverso su ciascun componente, quindi spezzandole fuori non avrebbe funzionato.
Naturalmente, se trovi una situazione in cui devi fare qualcosa solo per i componenti (diciamo) x dei vettori, potrebbe funzionare, tuttavia la pena di far tornare indietro tutto quando hai bisogno del vettore reale non sarebbe economica, quindi potresti mi chiedo se non dovresti usare i vettori per cominciare, ma solo una serie di float che accadono per consentire ai codici operativi vettoriali di fare i loro calcoli specifici.


2
Ti manca il punto di SoA per la matematica vettoriale. Raramente hai mai un solo oggetto su cui stai lavorando - in pratica stai iterando un array e facendo la stessa cosa con molti oggetti. Valuta di fare prodotti a 4 punti. Se stai memorizzando vettori come AoS in formato xyz0, prendere il punto di due vettori richiede multiply-shuffle-add-shuffle-add - 5 istruzioni. Fare prodotti a 4 punti richiede 20 istruzioni. D'altra parte, se hai 8 vettori memorizzati nella moda SoA (xxxx, yyyy, zzzz, xxxx, yyyy, zzzz) puoi fare 4 prodotti punto con solo 3 istruzioni (mul, madd, madd) - che è oltre 6 volte più veloce.
Charlie

Punto valido. Tuttavia, due osservazioni. Terrei sempre presente W in modo da non aver bisogno di 20 istruzioni, in secondo luogo, la maggior parte delle spese generali rimanenti può essere nascosta nella latenza di altre istruzioni - il tuo circuito chiuso soffrirebbe di gravi stalli della pipeline, no? fare 6 volte è un'ottimizzazione teorica. Quindi, mentre sì, vuoi raggruppare le tue operazioni - quasi mai dovrai solo fare un lotto rapido di prodotti punto senza nient'altro da fare su tali dati. Il costo di deswizzling / scatter dal lato PPU sarebbe un sacrificio troppo per me.
Kaj,

Gemendo, rimango corretto - su SPU avrei bisogno di 20 se fatto ingenuamente (ma mi rimescolerei sul posto). È una delle cose in cui ho finito per fare molti swizzles per ottenerlo ottimale. 360 ha un bel punto intrinseco (ma manca la straordinaria manipolazione dei bit).
Kaj,

Sì, ora che ci penso, se stai provando a fare "prodotti a 4 punti" puoi fare piuttosto meglio di 20 istruzioni perché puoi combinare alcune delle aggiunte successive. Ma avere i tuoi vettori nei registri come xxxx, yyyy, zzzz - sia che tu abbia spostato o memorizzato come SoA - elimini completamente questi shuffle. Ad ogni modo, hai ragione sul fatto che SoA rallenta il codice della logica ramificata, ma direi che la soluzione in molti casi del genere è quella di mettere a freno i tuoi dati e di trasformare la logica ramificata in piacevoli loop piatti.
Charlie

Concordato. Sono abbastanza sicuro che se ripasso il mio vecchio codice SPU (impossibile, società precedente) ci sono casi in cui l'ho spostato nel formato xxxxyyyyzzzz per l'ottimizzazione senza rendermene conto in modo specifico. Non l'ho mai offerto dal PPU in quel formato però. Intendiamoci, OP, cosa sta contemplando dma-ing x, y, z separatamente. Questo sicuramente non funzionerebbe per me. Vorrei anche (come ho fatto) preferirei muovere localmente poiché non tutto funziona meglio nel formato xxxxyyyyzzzz. Devo scegliere le tue battaglie, immagino. L'ottimizzazione per SPU è un vero spasso e ti senti terribilmente intelligente una volta che hai avuto quella soluzione stretta: o)
Kaj
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.