Unikać, jeśli instrukcje w modułach cieniujących DirectX 10?

14

Słyszałem, że jeśli należy unikać instrukcji w modułach cieniujących, ponieważ obie części instrukcji zostaną wykonane, a następnie niewłaściwe zostaną usunięte (co szkodzi wydajności).

To wciąż problem w DirectX 10? Ktoś mi powiedział, że w nim zostanie wykonana tylko odpowiednia gałąź.

Do ilustracji mam kod:

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;
}

Czy istnieje inny sposób na przyspieszenie?

Jeśli tak, jak to zrobić?

Obie gałęzie wyglądają podobnie, jedyną różnicą są wartości „stałych” ( y1, y2, b1, b2są takie same dla wszystkich pikseli w module Pixel Shader).

PolGraphic
źródło
1
Szczerze mówiąc, to bardzo przedwczesna optymalizacja, po prostu nie zmieniaj jej, dopóki nie przetestujesz kodu i nie będziesz w 100% uważać, że moduł cieniujący stanowi wąskie gardło.
pwny,

Odpowiedzi:

17

Wiele reguł dotyczących mikromoptymalizujących shaderów jest takich samych, jak w przypadku tradycyjnych procesorów z rozszerzeniami wektorowymi. Oto kilka wskazówek:

  • są wbudowane funkcje testowe ( test, lerp/ mix)
  • dodanie dwóch wektorów ma taki sam koszt jak dodanie dwóch liczb zmiennoprzecinkowych
  • swizzling jest bezpłatny

To prawda, że ​​gałęzie są tańsze na nowoczesnym sprzęcie niż kiedyś, ale nadal lepiej jest ich unikać, jeśli to możliwe. Korzystając z funkcji zamiatania i testowania, możesz przepisać shader bez testów:

/* 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));

Używanie stepi lerpjest bardzo powszechnym idiomem do wybierania między dwiema wartościami.

sam hocevar
źródło
6

Ogólnie jest OK. Shadery będą działać w grupach wierzchołków lub pikseli (różni dostawcy mają dla nich różną terminologię, więc trzymam się od tego z daleka), a jeśli wszystkie wierzchołki lub piksele w grupie idą tą samą ścieżką, koszt rozgałęzienia jest znikomy.

Musisz także zaufać kompilatorowi modułu cieniującego. Kod HLSL, który piszesz, nie powinien być postrzegany jako bezpośrednia reprezentacja kodu bajtowego lub nawet zestawu, do którego będzie się kompilował, a kompilator ma całkowitą swobodę konwersji na coś równoważnego, ale omija gałąź (np. Lerp może być czasem preferowana konwersja). Z drugiej strony, jeśli kompilator stwierdzi, że wykonanie gałęzi jest w rzeczywistości szybszą ścieżką, skompiluje ją do gałęzi. Przeglądanie wygenerowanego zestawu w PIX lub podobnym narzędziu może być bardzo pomocne.

Wreszcie, stara mądrość wciąż tu jest - profiluj ją, określ, czy to rzeczywiście problem z wydajnością, i rozwiąż ją wtedy, a nie wcześniej. Zakładając, że coś może być problemem związanym z wydajnością, i postępowanie zgodnie z tym założeniem będzie wiązało się z dużym ryzykiem wystąpienia większych problemów w przyszłości.

Maximus Minimus
źródło
4

Cytat z linku / artykułu opublikowanego przez Roberta Rouhani:

„Kody warunków (predykcja) są używane w starszych architekturach w celu emulacji prawdziwego rozgałęzienia. Instrukcje skompilowane dla tych architektur muszą oceniać zarówno odebrane, jak i nieprzyjęte instrukcje rozgałęzienia na wszystkich fragmentach. Warunek rozgałęzienia jest oceniany i ustawiany jest kod warunku. instrukcje w każdej części gałęzi muszą sprawdzić wartość kodu warunku przed zapisaniem ich wyników do rejestrów. W rezultacie tylko instrukcje w pobranych gałęziach zapisują swoje wyniki. Zatem w tych architekturach wszystkie gałęzie kosztują tyle, ile obie części gałąź plus koszty oceny stanu gałęzi. Rozgałęzianie powinno być stosowane oszczędnie na takich architekturach. Procesory graficzne NVIDIA GeForce z serii FX używają emulacji gałęzi kod warunkowy w swoich procesorach fragmentów. ”

Jak sugerował mh01 („Przeglądanie wygenerowanego zestawu w PIX lub podobnym narzędziu może być bardzo pomocne tutaj.”), Powinieneś użyć narzędzia kompilatora do zbadania danych wyjściowych. Z mojego doświadczenia wynika, że ​​narzędzie Cg nVidii (Cg jest nadal szeroko stosowane ze względu na jego możliwości wieloplatformowe) dało doskonałą ilustrację zachowania wymienionego w paragrafie kodów predykcji (predykcji) klejnotów GPU . Tak więc, niezależnie od wartości wyzwalacza, obie gałęzie były oceniane na podstawie fragmentów i tylko na końcu prawą umieszczono w rejestrze wyjściowym. Niemniej jednak czas obliczeń został zmarnowany. Wtedy myślałem, że rozgałęzienie pomoże wydajności, zwłaszcza że wszystkofragmenty w tym module cieniującym opierały się na jednolitej wartości przy podejmowaniu decyzji o właściwej gałęzi - co nie działało zgodnie z przeznaczeniem. Tak więc główne zastrzeżenie tutaj (np. Unikaj ubershaderów - być może największego źródła rozgałęzienia piekła).

teodron
źródło
2

Jeśli nie masz już problemów z wydajnością, w porządku. Koszt porównania z wartością stałą jest nadal niezwykle tani. Oto dobry artykuł na temat rozgałęziania GPU: http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter34.html

Niezależnie od tego, oto fragment kodu, który będzie działał znacznie gorzej niż instrukcja if (i jest znacznie mniej czytelny / możliwy do utrzymania), ale wciąż się go pozbywa:

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

x = 10 * y + b;

Zauważ, że zakładam, że x jest ograniczony do zakresu [0, 1]. To nie zadziała, jeśli x> = 2 lub x <0.

To, co wycina, to konwersja x na jeden 0lub 1pomnożenie niewłaściwego przez 0, a drugiego przez 1.

Robert Rouhani
źródło
Ponieważ pierwotnym testem jest if(x<0.5)wartość dla, fxpowinno być round(x)lub floor(x + 0.5).
sam hocevar
1

Istnieje wiele instrukcji umożliwiających wykonanie warunków bez rozgałęziania;

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);
}

Plus niektóre operatory logiczne;

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;
}

źródło: http://theorangeduck.com/page/avoiding-shader-conditionals

Alexis Paques
źródło