== causa la ramificazione in GLSL?


27

Cercando di capire esattamente cosa causa la ramificazione e cosa no in GLSL.

Lo sto facendo molto nel mio shader:

float(a==b)

Lo uso per simulare istruzioni if, senza diramazione condizionale ... ma è efficace? Non ho istruzioni if ​​da nessuna parte nel mio programma ora, né ho loop.

EDIT: Per chiarire, faccio cose come queste nel mio codice:

float isTint = float((renderflags & GK_TINT) > uint(0)); // 1 if true, 0 if false
    float isNotTint = 1-isTint;//swaps with the other value
    float isDarken = float((renderflags & GK_DARKEN) > uint(0));
    float isNotDarken = 1-isDarken;
    float isAverage = float((renderflags & GK_AVERAGE) > uint(0));
    float isNotAverage = 1-isAverage;
    //it is none of those if:
    //* More than one of them is true
    //* All of them are false
    float isNoneofThose = isTint * isDarken * isAverage + isNotTint * isAverage * isDarken + isTint * isNotAverage * isDarken + isTint * isAverage * isNotDarken + isNotTint * isNotAverage * isNotDarken;
    float isNotNoneofThose = 1-isNoneofThose;

    //Calc finalcolor;
    finalcolor = (primary_color + secondary_color) * isTint * isNotNoneofThose + (primary_color - secondary_color) * isDarken * isNotNoneofThose + vec3((primary_color.x + secondary_color.x)/2.0,(primary_color.y + secondary_color.y)/2.0,(primary_color.z + secondary_color.z)/2.0) * isAverage * isNotNoneofThose + primary_color * isNoneofThose;

EDIT: so perché non voglio ramificare. So cos'è la ramificazione. Sono contento che stai insegnando ai bambini le ramificazioni ma mi piacerebbe conoscere me stesso degli operatori booleani (e operazioni bit a bit ma sono abbastanza sicuro che vadano bene)

Risposte:


42

Le cause della ramificazione in GLSL dipendono dal modello GPU e dalla versione del driver OpenGL.

La maggior parte delle GPU sembra avere una forma di operazione "seleziona uno dei due valori" che non ha costi di ramificazione:

n = (a==b) ? x : y;

e a volte anche cose come:

if(a==b) { 
   n = x;
   m = y;
} else {
   n = y;
   m = x;
}

sarà ridotto ad alcune operazioni a valore selezionato senza penalità di ramificazione.

Alcuni GPU / Driver hanno (avuto?) Un po 'di penalità sull'operatore di confronto tra due valori ma un'operazione più veloce sul confronto contro zero.

Dove potrebbe essere più veloce fare:

gl_FragColor.xyz = ((tmp1 - tmp2) != vec3(0.0)) ? E : tmp1;

piuttosto che confrontare (tmp1 != tmp2)direttamente ma questo dipende molto dalla GPU e dal driver, quindi a meno che tu non stia prendendo di mira una GPU molto specifica e nessun altro ti consiglio di utilizzare l'operazione di confronto e lasciare quel lavoro di ottimizzazione al driver OpenGL poiché un altro driver potrebbe avere un problema con la forma più lunga ed essere più veloce con il modo più semplice e più leggibile.

Anche i "rami" non sono sempre una brutta cosa. Ad esempio sulla GPU SGX530 utilizzata in OpenPandora, questo shader scale2x (30ms):

    lowp vec3 E = texture2D(s_texture0, v_texCoord[0]).xyz;
    lowp vec3 D = texture2D(s_texture0, v_texCoord[1]).xyz;
    lowp vec3 F = texture2D(s_texture0, v_texCoord[2]).xyz;
    lowp vec3 H = texture2D(s_texture0, v_texCoord[3]).xyz;
    lowp vec3 B = texture2D(s_texture0, v_texCoord[4]).xyz;
    if ((D - F) * (H - B) == vec3(0.0)) {
            gl_FragColor.xyz = E;
    } else {
            lowp vec2 p = fract(pos);
            lowp vec3 tmp1 = p.x < 0.5 ? D : F;
            lowp vec3 tmp2 = p.y < 0.5 ? H : B;
            gl_FragColor.xyz = ((tmp1 - tmp2) != vec3(0.0)) ? E : tmp1;
    }

Finì drammaticamente più veloce di questo shader equivalente (80ms):

    lowp vec3 E = texture2D(s_texture0, v_texCoord[0]).xyz;
    lowp vec3 D = texture2D(s_texture0, v_texCoord[1]).xyz;
    lowp vec3 F = texture2D(s_texture0, v_texCoord[2]).xyz;
    lowp vec3 H = texture2D(s_texture0, v_texCoord[3]).xyz;
    lowp vec3 B = texture2D(s_texture0, v_texCoord[4]).xyz;
    lowp vec2 p = fract(pos);

    lowp vec3 tmp1 = p.x < 0.5 ? D : F;
    lowp vec3 tmp2 = p.y < 0.5 ? H : B;
    lowp vec3 tmp3 = D == F || H == B ? E : tmp1;
    gl_FragColor.xyz = tmp1 == tmp2 ? tmp3 : E;

Non si sa mai in anticipo come si comporteranno un compilatore GLSL specifico o una GPU specifica fino a quando non lo si confronta.


Per aggiungere il punto to (anche se non ho numeri di temporizzazione effettivi e codice shader per presentarti per questa parte) attualmente uso come normale hardware di test:

  • Intel HD Graphics 3000
  • Grafica Intel HD 405
  • nVidia GTX 560M
  • nVidia GTX 960
  • AMD Radeon R7 260X
  • nVidia GTX 1050

Come una vasta gamma di modelli GPU diversi e comuni con cui testare.

Test di ciascuno con driver OpenGL e OpenCL open source per Windows, proprietari di Linux e Linux.

E ogni volta che provo a micro-ottimizzare lo shader GLSL (come nell'esempio di SGX530 sopra) o le operazioni OpenCL per una particolare combinazione di GPU / Driver finisco per danneggiare ugualmente le prestazioni su più di una delle altre GPU / Driver.

Quindi, oltre a ridurre chiaramente la complessità matematica di alto livello (ad esempio: convertire 5 divisioni identiche in un unico reciproco e 5 moltiplicazioni) e ridurre le ricerche di trama / larghezza di banda, molto probabilmente sarà una perdita di tempo.

Ogni GPU è troppo diversa dalle altre.

Se lavorassi specificamente su (a) console di gioco con una GPU specifica, questa sarebbe una storia diversa.

L'altro aspetto (meno significativo per gli sviluppatori di giochi di piccole dimensioni ma comunque notevole) è che i driver GPU per computer potrebbero un giorno sostituire silenziosamente i tuoi shader ( se il tuo gioco diventa abbastanza popolare ) con quelli riscritti personalizzati ottimizzati per quella particolare GPU. Fare tutto questo funziona per te.

Lo faranno per giochi popolari che vengono spesso usati come benchmark.

Oppure, se dai ai tuoi giocatori l'accesso agli shader in modo che possano facilmente modificarli, alcuni di loro potrebbero spremere qualche FPS in più a proprio vantaggio.

Ad esempio, ci sono shader e texture pack creati dai fan per Oblivion per aumentare notevolmente la frequenza dei fotogrammi su hardware altrimenti a malapena riproducibile.

E infine, una volta che lo shader diventa abbastanza complesso, il gioco è quasi completato e inizi a testare su hardware diverso, sarai abbastanza occupato solo a sistemare gli shader in modo che funzionino su una varietà di GPU poiché è dovuto a vari bug che non vuoi avere tempo per ottimizzarli a quel livello.


"O se dai ai tuoi giocatori l'accesso agli shader in modo che possano facilmente modificarli da soli ..." Dato che hai menzionato questo, quale potrebbe essere il tuo approccio agli shader wallhack e simili? Sistema d'onore, verificato, rapporti ...? Mi piace l'idea di lobby limitate agli stessi shader / asset, qualunque essi siano, dal momento che le posizioni sul realismo max / min / scalabile, gli exploit e così via dovrebbero riunire giocatori e modder per incoraggiare la revisione, la collaborazione, ecc. per ricordare che questo è il modo in cui ha funzionato la Mod di Gary, ma sono fuori dal giro.
John P,

1
@JohnP La sicurezza di tutto ciò che presuppone che il client non sia compromesso non funziona comunque. Naturalmente se non vuoi che le persone modifichino i loro shader non ha senso esporli, ma in realtà non aiuta molto con la sicurezza. La tua strategia per rilevare cose come i wallhack dovrebbe trattare il disordine lato client con le cose come una prima barriera bassa, e probabilmente ci potrebbe essere un vantaggio maggiore per consentire il modding leggero come in questa risposta se non porta a un vantaggio ingiusto rilevabile per il giocatore .
Cubico

8
@JohnP Se non vuoi che anche i giocatori vedano attraverso i muri, non lasciare che il server invii loro informazioni su ciò che si trova dietro il muro.
Polygnome,

1
È proprio così: non sono contrario all'hacking a muro tra giocatori a cui piace per qualsiasi motivo. Come giocatore, però, ho abbandonato diversi titoli AAA perché - tra le altre ragioni - hanno fatto esempi di modder estetici mentre denaro / XP / ecc. gli hacker sono rimasti incolumi (che hanno guadagnato soldi veri da quelli abbastanza frustrati da pagare), hanno a corto di personale e automatizzato il loro sistema di segnalazione e appello, e si sono assicurati che i giochi vivessero e morissero per il numero di server che tenevano a mantenere in vita. Speravo che ci potesse essere un approccio più decentralizzato sia come sviluppatore che come giocatore.
John P,

No, non faccio in linea se da qualche parte. Faccio solo float (dichiarazione booleana) * (qualcosa)
Geklmintendon't of Awesome

7

La risposta di @Stephane Hockenhull ti dà praticamente quello che devi sapere, dipenderà interamente dall'hardware.

Ma permettetemi di darvi alcuni esempi di come si può essere dipendenti dall'hardware, e perché la ramificazione è anche un problema a tutti, che cosa fa la GPU fare dietro le quinte quando ramificazione fa prendere posto.

Il mio focus è principalmente su Nvidia, ho una certa esperienza con la programmazione CUDA di basso livello e vedo cosa viene generato PTX ( IR per kernel CUDA , come SPIR-V ma solo per Nvidia) e vedo i parametri di riferimento per apportare alcune modifiche.

Perché Branching in GPU Architectures è un grosso problema?

Perché è male ramificarsi in primo luogo? Perché le GPU cercano di evitare di ramificarsi in primo luogo? Poiché le GPU utilizzano in genere uno schema in cui i thread condividono lo stesso puntatore di istruzioni . Le GPU seguono un'architettura SIMDtipicamente, e mentre la granularità di ciò può cambiare (cioè 32 thread per Nvidia, 64 per AMD e altri), a un certo livello un gruppo di thread condivide lo stesso puntatore di istruzione. Ciò significa che quei thread devono guardare la stessa riga di codice per poter lavorare insieme sullo stesso problema. Potresti chiedere come sono in grado di utilizzare le stesse righe di codice e fare cose diverse? Usano valori diversi nei registri, ma quei registri sono ancora usati nelle stesse righe di codice nell'intero gruppo. Cosa succede quando smette di essere così? (IE a branch?) Se il programma non ha davvero modo di aggirarlo, divide il gruppo (Nvidia tali fasci di 32 thread sono chiamati Warp , per AMD e il mondo del calcolo parallelo, viene chiamato wavefront) in due o più gruppi diversi.

Se ci sono solo due diverse righe di codice su cui finire, i thread di lavoro sono suddivisi tra due gruppi (da qui uno li chiamerò orditi). Supponiamo l'architettura Nvidia, dove la dimensione del filo di ordito è 32, se la metà di questi fili diverge, allora avrai 2 orditi occupati da 32 fili attivi, il che rende le cose la metà efficienti da un computazionale a un fine. Su molte architetture, la GPU proverà a rimediare a questo facendo convergere i thread in un singolo warp una volta che raggiungono lo stesso ramo post istruzione, oppure il compilatore inserirà esplicitamente un punto di sincronizzazione che dice alla GPU di riconvertire i thread o tentare di farlo.

per esempio:

if(a)
    x += z * w;
    q >>= p;
else if(c)
    y -= 3;
r += t;

Il thread ha un forte potenziale di divergenza (percorsi di istruzione diversi), in tal caso si potrebbe verificare la convergenza in r += t;cui i puntatori di istruzione sarebbero nuovamente gli stessi. La divergenza può verificarsi anche con più di due rami, con conseguente utilizzo dell'ordito anche inferiore, quattro rami significa che 32 fili vengono suddivisi in 4 orditi, il 25% di utilizzo della produzione. La convergenza, tuttavia, può nascondere alcuni di questi problemi, poiché il 25% non mantiene il throughput nell'intero programma.

Su GPU meno sofisticate, possono verificarsi altri problemi. Invece di divergere calcolano semplicemente tutti i rami, quindi selezionano l'output alla fine. Ciò potrebbe apparire uguale alla divergenza (entrambi hanno un utilizzo della velocità effettiva di 1 / n), ma ci sono alcuni problemi importanti con l'approccio della duplicazione.

Uno è il consumo di energia, stai usando molta più energia ogni volta che accade un ramo, questo sarebbe un male per gpus mobile. Il secondo è che la divergenza si verifica solo su Nvidia gpus quando i fili dello stesso ordito prendono percorsi diversi e quindi hanno un puntatore di istruzioni diverso (che è condiviso come di pascal). Quindi puoi ancora avere ramificazioni e non avere i problemi di throughput sulle GPU Nvidia se si verificano in multipli di 32 o si verificano solo in un singolo warp su dozzine. se è probabile che succeda un ramo, è più probabile che un numero inferiore di thread diverga e non si avrà comunque un problema di ramificazione.

Un altro problema minore è quando si confrontano le GPU rispetto alle CPU, spesso non hanno meccanismi di previsione e altri robusti meccanismi di diramazione a causa della quantità di hardware occupata da tali meccanismi, spesso è possibile vedere il riempimento no-op sulle GPU moderne a causa di ciò.

Esempio pratico di differenza architettonica della GPU

Ora facciamo l'esempio di Stephanes e vediamo come sarebbe l'assemblaggio per soluzioni senza diramazione su due architetture teoriche.

n = (a==b) ? x : y;

Come ha detto Stephane, quando il compilatore del dispositivo incontra un ramo può decidere di usare un'istruzione per "scegliere" un elemento che finirebbe per non avere penalità di ramo. Ciò significa che su alcuni dispositivi questo sarebbe compilato in qualcosa del genere

cmpeq rega, regb
// implicit setting of comparison bit used in next part
choose regn, regx, regy

su altri senza un'istruzione scelta, potrebbe essere compilato

n = ((a==b))* x + (!(a==b))* y

che potrebbe apparire come:

cmpeq rega regb
// implicit setting of comparison bit used in next part
mul regn regcmp regx
xor regcmp regcmp 1
mul regresult regcmp regy
mul regn regn regresult

che è senza rami ed equivalente, ma richiede molte più istruzioni. Poiché l'esempio di Stephanes sarà probabilmente compilato su entrambi i rispettivi sistemi, non ha molto senso provare a capire manualmente la matematica per rimuovere noi stessi la ramificazione, poiché il compilatore della prima architettura potrebbe decidere di compilare nella seconda forma invece di la forma più veloce.


5

Concordo con tutto quanto detto nella risposta di @Stephane Hockenhull. Per espandere l'ultimo punto:

Non si sa mai in anticipo come si comporteranno un compilatore GLSL specifico o una GPU specifica fino a quando non lo si confronta.

Assolutamente vero. Inoltre, vedo questo tipo di domanda sorgere abbastanza frequentemente. Ma in pratica raramente ho visto uno shader di frammenti essere la fonte di un problema di prestazioni. È molto più comune che altri fattori stiano causando problemi come troppe letture di stato dalla GPU, scambio di troppi buffer, troppo lavoro in una singola chiamata di disegno, ecc.

In altre parole, prima di preoccuparti della micro-ottimizzazione di uno shader, profila l'intera app e assicurati che gli shader siano ciò che sta causando il tuo rallentamento.

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.