Evitare se le istruzioni negli shader DirectX 10?


14

Ho sentito che se le dichiarazioni dovessero essere evitate negli shader, poiché entrambe le parti delle istruzioni verranno eseguite e verrà eliminato il torto (il che danneggerebbe le prestazioni).

È ancora un problema in DirectX 10? Qualcuno mi ha detto che in esso verrà eseguito solo il ramo giusto.

Per l'illustrazione ho il codice:

float y1 = 5; float y2 = 6; float b1 = 2; float b2 = 3;

if(x>0.5){
    x = 10 * y1 + b1;
}else{
    x = 10 * y2 + b2;
}

C'è un altro modo per renderlo più veloce?

In tal caso, come si fa?

Entrambi i rami sembrano simili, l'unica differenza sono i valori di "costanti" ( y1, y2, b1, b2sono gli stessi per tutti i pixel in Pixel Shader).


1
Onestamente, questa è un'ottimizzazione molto prematura, semplicemente non cambiarli fino a quando non hai confrontato il tuo codice e hai il 100% che lo shader è un collo di bottiglia.
pwny

Risposte:


17

Molte regole per gli shader di micro-ottimizzazione sono le stesse delle CPU tradizionali con estensioni vettoriali. Ecco alcuni suggerimenti:

  • ci sono funzioni di test integrate ( test, lerp/ mix)
  • l'aggiunta di due vettori ha lo stesso costo dell'aggiunta di due galleggianti
  • lo swizzling è gratuito

È vero che i rami costano meno sull'hardware moderno di quanto non fossero in passato, ma è ancora meglio evitarli se possibile. Utilizzando le funzioni di swizzling e test è possibile riscrivere lo shader senza test:

/* y1, y2, b1, b2 */
float4 constants = float4(5, 6, 2, 3);

float2 tmp = 10 * constants.xy + constants.zw;
x = lerp(tmp[1], tmp[0], step(x, 0.5));

Usare stepe lerpè un linguaggio molto comune per scegliere tra due valori.


6

In generale va bene. Gli shader verranno eseguiti in gruppi di vertici o pixel (diversi fornitori hanno una terminologia diversa per questi, quindi me ne sto alla larga) e se tutti i vertici o i pixel di un gruppo prendono lo stesso percorso, il costo di ramificazione è trascurabile.

Devi anche fidarti del compilatore shader. Il codice HLSL che scrivi non dovrebbe essere visto come una rappresentazione diretta del bytecode o anche dell'assembly che verrà compilato, e il compilatore è perfettamente libero di convertirlo in qualcosa di equivalente ma evita il ramo (ad esempio un lerp a volte può essere una conversione preferita). D'altra parte, se il compilatore determina che l'esecuzione di un ramo è in realtà il percorso più veloce, lo compilerà fino a un ramo. La visualizzazione dell'assieme generato in PIX o in uno strumento simile può essere molto utile qui.

Infine, la vecchia saggezza è ancora valida: profilalo, determina se in realtà è un problema di prestazioni per te e affrontalo, non prima. Supponendo che qualcosa possa essere un problema di prestazioni e agire in base a tale presupposto comporterà in seguito un rischio enorme di problemi più grandi.


4

Citazione dal link / articolo pubblicato da Robert Rouhani:

"I codici di condizione (previsione) vengono utilizzati nelle architetture più vecchie per emulare il vero branching. Le istruzioni if-then compilate per queste architetture devono valutare sia le istruzioni di ramo prese che non prese su tutti i frammenti. La condizione di ramo viene valutata e viene impostato un codice di condizione. le istruzioni in ogni parte del ramo devono verificare il valore del codice condizione prima di scrivere i loro risultati nei registri. Di conseguenza, solo le istruzioni nei rami presi scrivono il loro output. Pertanto, in queste architetture tutti i rami costano quanto entrambe le parti del branch, oltre al costo della valutazione delle condizioni del branch. Il branching dovrebbe essere usato con parsimonia su tali architetture. Le GPU NVIDIA GeForce FX Series usano l'emulazione branch con codice di condizione nei loro processori di frammenti. "

Come suggerito mh01 ("La visualizzazione dell'assembly generato in PIX o uno strumento simile può essere molto utile qui."), È necessario utilizzare uno strumento di compilazione per esaminare l'output. Nella mia esperienza, lo strumento Cg di nVidia (Cg è ancora oggi ampiamente utilizzato a causa delle sue capacità multipiattaforma) ha fornito una perfetta illustrazione del comportamento menzionato nel paragrafo Codici di condizione delle gemme della GPU (predicazione) . Pertanto, indipendentemente dal valore del trigger, entrambi i rami sono stati valutati su una base per frammento e solo alla fine, quello giusto è stato inserito nel registro di output. Tuttavia, il tempo di calcolo è stato sprecato. Allora, ho pensato che la ramificazione avrebbe aiutato le prestazioni, soprattutto perché tuttoi frammenti di quello shader si basavano su un valore uniforme per decidere sul ramo giusto - ciò non avveniva come previsto. Quindi, un avvertimento importante qui (ad esempio evitare ubershaders - forse la più grande fonte di inferno ramificato).


2

Se non hai già problemi di prestazioni, va bene. Il costo per il confronto con una costante è ancora estremamente economico. Ecco una buona lettura sulla ramificazione della GPU: http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter34.html

Indipendentemente da ciò, ecco uno snippet di codice che preformerà molto peggio dell'istruzione if (ed è molto meno leggibile / gestibile), ma ancora si sbarazza di esso:

int fx = floor(x);
int y = (fx * y2) + ((1- fx) * y1);
int b = (fx * b2) + ((1 -fx) * b1);

x = 10 * y + b;

Si noti che sto assumendo che x sia limitato all'intervallo [0, 1]. Questo non funzionerà se x> = 2 o x <0.

Quello che fa lo snipped è convertire x in uno 0o 1e moltiplicare quello sbagliato per 0 e l'altro per 1.


Poiché il test originale è if(x<0.5)il valore per fxdovrebbe essere round(x)o floor(x + 0.5).
Sam Hocevar,

1

Ci sono più istruzioni in grado di fare condizioni senza ramificazione;

vec4 when_eq(vec4 x, vec4 y) {
  return 1.0 - abs(sign(x - y));
}

vec4 when_neq(vec4 x, vec4 y) {
  return abs(sign(x - y));
}

vec4 when_gt(vec4 x, vec4 y) {
  return max(sign(x - y), 0.0);
}

vec4 when_lt(vec4 x, vec4 y) {
  return max(sign(y - x), 0.0);
}

vec4 when_ge(vec4 x, vec4 y) {
  return 1.0 - when_lt(x, y);
}

vec4 when_le(vec4 x, vec4 y) {
  return 1.0 - when_gt(x, y);
}

Inoltre alcuni operatori logici;

vec4 and(vec4 a, vec4 b) {
  return a * b;
}

vec4 or(vec4 a, vec4 b) {
  return min(a + b, 1.0);
}

vec4 xor(vec4 a, vec4 b) {
  return (a + b) % 2.0;
}

vec4 not(vec4 a) {
  return 1.0 - a;
}

fonte: http://theorangeduck.com/page/avoiding-shader-conditionals

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.