Perché i confronti sono così costosi su una GPU?


10

Durante il tentativo di migliorare le prestazioni della mia classe di rilevamento delle collisioni, ho scoperto che circa l'80% del tempo trascorso alla GPU, ha speso in condizioni if ​​/ else solo cercando di capire i limiti per i secchi attraverso i quali dovrebbe passare.

Più precisamente:

  1. ogni thread ottiene un ID, da quell'ID recupera il suo triangolo dalla memoria (3 interi ciascuno) e da quei 3 recupera i suoi vertici (3 float ciascuno).

  2. Quindi trasforma i vertici in punti della griglia interi (attualmente 8x8x8) e li trasforma nei limiti del triangolo su quella griglia

  3. Per trasformare i 3 punti in limiti, trova il min / max di ogni dimensione tra ciascuno dei punti

Poiché al linguaggio di programmazione che sto usando manca un minmax intrinseco, ne ho creato uno anch'io, simile a questo:

procedure MinMax(a, b, c):
   local min, max

   if a > b:
      max = a
      min = b
   else:
      max = b
      min = a
   if c > max:
      max = c
   else:
      if c < min:
         min = c

   return (min, max)

Quindi in media dovrebbe essere 2,5 * 3 * 3 = 22,5 confronti che finiscono per consumare molto più tempo rispetto ai test di intersezione triangolo - bordo effettivi (circa 100 * 11-50 istruzioni).

In effetti, ho scoperto che il pre-calcolo dei bucket richiesti sulla CPU (single threaded, nessuna vettorializzazione), impilandoli in una vista gpu insieme alla definizione del bucket e facendo fare alla gpu ~ 4 letture extra per thread era 6 volte più veloce del provare per capire i limiti sul posto. (nota che vengono ricalcolati prima di ogni esecuzione poiché ho a che fare con mesh dinamiche)

Allora, perché il confronto è così orrendamente lento su una GPU?


2
La tua domanda riguarda le prestazioni a livello di istruzione di uno specifico pezzo di codice su un tipo specifico di hardware. A me sembra molto più una domanda di programmazione che una domanda di informatica.
David Richerby,

7
La mia ipotesi è che non sono i confronti che sono costosi ma i rami. Se il compilatore non utilizza la previsione (o la GPU non fornisce tale), verrebbero utilizzati i rami che causano il fork "thread" (poiché le GPU sono orientate al SIMD). Convertire la condizione in maschera e usare la maschera per sintetizzare mosse / scambi condizionali può essere un'alternativa ragionevole.
Paul A. Clayton,

1
@DavidRicherby Non sono sicuro che sia così specifico. Questa domanda non si applicherebbe a nessuna architettura SIMD?
Kasperd,

1
@DavidRicherby: il motivo per cui insegniamo comp arch nei reparti CS è perché l'arco comp ha un impatto sugli algoritmi scelti. Le architetture SIMD possono produrre un throughput elevato solo se riesci a capire come scrivere il programma senza rami nidificati.
Wandering Logic,

2
Come afferma la risposta di Wandering Logic in un modo meno ovvio, le GPU funzionano presupponendo che molti "thread" siano alla stessa istruzione contemporaneamente. Quindi le GPU, in parole povere, prendono ogni ramo anziché solo i rami veri. Questo è il motivo per cui le GPU sfruttano il fatto che i vicini di solito prendono gli stessi rami; e le prestazioni sono terribili quando questo non è vero.
Rob,

Risposte:


10

Le GPU sono architetture SIMD. Nelle architetture SIMD ogni istruzione deve essere eseguita per ogni elemento che si elabora. (C'è un'eccezione a questa regola, ma raramente aiuta).

Quindi nella tua MinMaxroutine non solo ogni chiamata deve recuperare tutte e tre le istruzioni di diramazione (anche se in media vengono valutate solo 2,5), ma anche ogni istruzione di assegnazione occupa un ciclo (anche se in realtà non viene "eseguita" ).

Questo problema viene talvolta chiamato divergenza di thread . Se la tua macchina ha qualcosa come 32 corsie di esecuzione SIMD, avrà comunque una sola unità di recupero. (Qui il termine "thread" in pratica significa "corsia di esecuzione SIMD".) Quindi internamente ogni corsia di esecuzione SIMD ha un bit "Sono abilitato / disabilitato" e i rami in realtà manipolano quel bit. (L'eccezione è che nel punto in cui ogni corsia SIMD viene disabilitata, l'unità di recupero generalmente salterà direttamente alla clausola "else".)

Quindi nel tuo codice, ogni corsia di esecuzione SIMD sta facendo:

compare (a > b)
assign (max = a if a>b)
assign (min = b if a>b)
assign (max = b if not(a>b))
assign (min = a if not(a>b))
compare (c > max)
assign (max = c if c>max)
compare (c < min if not(c>max))
assign (min = c if not(c>max) and c<min)

Può accadere che su alcune GPU questa conversione dei condizionali in previsione sia più lenta se la GPU lo fa da sola. Come sottolineato da @ PaulA.Clayton, se il tuo linguaggio di programmazione e architettura hanno un'operazione di spostamento condizionale prevista (in particolare uno dei moduli if (c) x = y else x = z), potresti essere in grado di fare meglio. (Ma probabilmente non molto meglio).

Inoltre, non è necessario posizionare il c < mincondizionale all'interno di elseof c > max. Certamente non ti sta salvando nulla e (dato che la GPU deve convertirlo automaticamente in predicazione) potrebbe effettivamente essere doloroso averlo annidato in due diversi condizionali.


2
(Scusate se una parte di questo non è chiara, sto cercando di ottenere una risposta prima che i teorici chiudano la domanda come fuori tema.)
Logica errante


È in tema nel senso che alcuni algoritmi non possono essere accelerati attraverso il parallelismo SIMD. (ad esempio: Lavoro, Span, ecc. per un trattamento più teorico del perché)
Rob

1
Ecco un'altra lezione sulle basi della divergenza people.maths.ox.ac.uk/gilesm/cuda/lecs/lec3-2x2.pdf Nota da questi che il problema (su Nvidia comunque) è solo per-ordito. Il codice in esecuzione su orditi diversi può divergere felicemente. E un altro documento che propone un metodo per evitarlo: hal.inria.fr/file/index/docid/649650/filename/sbiswi.pdf
Fizz

Su una virata leggermente diversa, ma in linea con i commenti che ho scritto sotto la domanda eprint.iacr.org/2012/137.pdf vale la pena leggere: un rallentamento di 10 volte rispetto alle prestazioni previste può essere "normale" per una GPU a meno che non ci si abbassi al suo assemblaggio (di solito con strumenti ufficialmente non supportati). È possibile che i compilatori con targeting per GPU siano migliorati, ma non trattengo il respiro.
Fizz,
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.