Le dichiarazioni If rallentano il mio shader?

Voglio sapere se le “istruzioni if” all’interno degli shader (vertice / frammento / pixel …) stanno davvero rallentando le prestazioni dello shader. Per esempio:

È meglio usare questo:

vec3 output; output = input*enable + input2*(1-enable); 

invece di usare questo:

 vec3 output; if(enable == 1) { output = input; } else { output = input2; } 

in un altro forum c’è stato un discorso su questo (2013): http://answers.unity3d.com/questions/442688/shader-if-else-performance.html Qui i ragazzi stanno dicendo che le dichiarazioni If sono davvero cattive per le prestazioni dello shader.

Anche qui stanno parlando di quanto c’è dentro le dichiarazioni if ​​/ else (2012): https://www.opengl.org/discussion_boards/showthread.php/177762-Performance-alternative-for-if-(-)

forse l’hardware o lo shadercompiler sono migliori ora e correggono in qualche modo questo (forse non esistente) problema di prestazioni.

MODIFICARE:

Che cosa è con questo caso, qui lascia dire che enable è una variabile uniforms ed è sempre impostato su 0:

 if(enable == 1) //never happens { output = vec4(0,0,0,0); } else //always happens { output = calcPhong(normal, lightDir); } 

Penso che qui abbiamo un ramo all’interno dello shader che rallenta lo shader. È corretto?

Ha più senso creare 2 diversi shader come uno per l’altro e l’altro per la parte if?

Che cosa si tratta degli shader che anche potenzialmente possono generare problemi di performance delle istruzioni? Ha a che fare con come gli shader vengono eseguiti e da dove le GPU traggono le loro massime prestazioni di elaborazione.

Le invocazioni separate dello shader vengono solitamente eseguite in sequenza, eseguendo le stesse istruzioni contemporaneamente. Li stanno semplicemente eseguendo su diversi set di valori di input; condividono uniformi, ma hanno variabili interne differenti. Un termine per un gruppo di shader che eseguono tutte la stessa sequenza di operazioni è “wavefront”.

Il potenziale problema con qualsiasi forma di ramificazione condizionale è che può rovinare tutto. Fa sì che diverse invocazioni all’interno del fronte d’onda debbano eseguire sequenze di codice diverse. Questo è un processo molto costoso, per cui è necessario creare un nuovo fronte d’onda, copiare i dati su di esso, ecc.

A meno che … non lo faccia.

Ad esempio, se la condizione è quella che viene presa da ogni chiamata nel fronte d’onda, non è necessaria alcuna divergenza di runtime. In quanto tale, il costo del if è solo il costo del controllo di una condizione.

Qui ci sono diversi casi, che rappresentano ciò che il tuo codice assomiglia al compilatore:

  • Ramificazione statica. La condizione è basata su costanti in fase di compilazione; come tale, sapete guardando il codice e sapere quali rami saranno presi. Praticamente qualsiasi compilatore gestisce questo come parte dell’ottimizzazione di base.
  • Ramificazione statica uniforms. La condizione si basa su espressioni che coinvolgono solo uniformi o costanti. Non si può sapere a priori quale ramo verrà preso, ma il compilatore può essere staticamente sicuro che i wavefronts non verranno mai interrotti da questo if .
  • Ramificazione dynamic. La condizione si basa su espressioni che coinvolgono più di semplici costanti e uniformi. Qui, un compilatore non può dire a priori se un fronte d’onda sarà rotto o no. Se ciò accade dipende dalla valutazione runtime dell’espressione di condizione.

Hardware diverso può gestire diversi tipi di ramificazione senza divergenze.

Inoltre, anche se una condizione è presa da diversi fronti d’onda, si potrebbe ristrutturare il codice per non richiedere la ramificazione effettiva. Hai dato un buon esempio: output = input*enable + input2*(1-enable); è funzionalmente equivalente all’istruzione if . Un compilatore potrebbe rilevare che un if viene usato per impostare una variabile, e quindi eseguire entrambi i lati. Questo viene fatto spesso nei casi in cui l’hardware non è in grado di stabilire se una condizione può essere eseguita senza divergenze, ma i corpi delle due condizioni sono piccoli.

Praticamente tutto l’hardware può gestire var = bool ? val1 : val2 var = bool ? val1 : val2 senza dover divergere. Questo è stato ansible nel lontano 2002.

Dato che dipende molto dall’hardware, dipende dall’hardware. Ci sono tuttavia alcune epoche di hardware che possono essere viste:

Desktop, Pre-D3D10

Lì, è un po ‘selvaggio west. Il compilatore di NVIDIA per questo tipo di hardware era noto per rilevare tali condizioni e in realtà ricompilare lo shader ogni volta che cambiava uniformi che influivano su tali condizioni.

In generale, questa era è dove viene circa l’80% delle “dichiarazioni mai usate”. Ma anche qui, non è necessariamente vero.

Puoi aspettarti l’ottimizzazione della ramificazione statica. Potete sperare che una ramificazione statica e uniforms non causerà alcun ulteriore rallentamento (anche se il fatto che NVIDIA pensasse che la ricompilazione sarebbe stata più veloce di eseguirla rende improbabile, almeno per il loro hardware). Ma la ramificazione dynamic ti costerà qualcosa, anche se tutte le invocazioni prendono lo stesso ramo.

I compilatori di questa epoca fanno del loro meglio per ottimizzare gli shader in modo che le semplici condizioni possano essere eseguite semplicemente. Ad esempio, output = input*enable + input2*(1-enable); è qualcosa che un compilatore decente potrebbe generare dalla tua istruzione if equivalente.

Desktop, Post-D3D10

L’hardware di questa era è generalmente in grado di gestire dichiarazioni di rami staticamente uniformi con un piccolo rallentamento. Per le diramazioni dinamiche, è ansible che si verifichi o meno un rallentamento.

Desktop, D3D11 +

L’hardware di questa era è praticamente garantito per essere in grado di gestire condizioni dynamicmente uniformi con piccoli problemi di prestazioni. In effetti, non ha nemmeno bisogno di essere dynamicmente uniforms; fintanto che tutte le invocazioni all’interno dello stesso fronte d’onda seguono lo stesso percorso, non si verificherà alcuna perdita significativa delle prestazioni.

Si noti che alcuni componenti hardware dell’epoca precedente probabilmente potrebbero farlo anche in questo caso. Ma questo è quello in cui è quasi certo che sia vero.

Mobile, ES 2.0

Bentornato nel selvaggio west. Sebbene diversamente dal desktop Pre-D3D10, ciò è dovuto principalmente all’enorme varianza dell’hardware calibro ES 2.0. C’è una tale quantità di cose che possono gestire ES 2.0, e funzionano tutte in modo molto diverso l’una dall’altra.

La ramificazione statica sarà probabilmente ottimizzata. Ma se si ottiene una buona prestazione da una ramificazione statica uniforms è molto dipendente dall’hardware.

Mobile, ES 3.0+

L’hardware qui è piuttosto maturo e capace di ES 2.0. Di conseguenza, è ansible prevedere che i rami staticamente uniformi vengano eseguiti in modo abbastanza ragionevole. E alcuni hardware possono probabilmente gestire i rami dinamici come fa il moderno hardware del desktop.

Dipende molto dall’hardware e dalle condizioni.

Se la tua condizione è uniforms: non preoccuparti, lascia che sia il compilatore a occuparsene. Se la tua condizione è qualcosa di dinamico (come un valore calcolato da un attributo o prelevato da una trama o qualcosa del genere), allora è più complicato.

Per quest’ultimo caso dovrai testare e fare benchmark perché dipenderà dalla complessità del codice in ogni branch e da quanto “coerente” sia la decisione del branch.

Ad esempio, se uno dei rami viene prelevato al 99% del caso e scarta il frammento, molto probabilmente si desidera mantenere il condizionale. Ma OTOH nel tuo semplice esempio sopra se l’ enable è una condizione dynamic, la selezione aritmetica potrebbe essere migliore.

A meno che tu non abbia un caso ben definito come sopra, oa meno che tu non stia ottimizzando per una determinata architettura nota, probabilmente stai meglio dalla figura del compilatore per te.