Czy == powoduje rozgałęzienie w GLSL?


27

Próbując dowiedzieć się dokładnie, co powoduje rozgałęzienie, a co nie w GLSL.

Często robię to w moim module cieniującym:

float(a==b)

Używam go do symulacji instrukcji if bez rozgałęzienia warunkowego ... ale czy jest to skuteczne? Nie mam teraz instrukcji if w moim programie, ani nie mam żadnych pętli.

EDYCJA: Aby to wyjaśnić, robię takie rzeczy w moim kodzie:

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;

EDYCJA: Wiem, dlaczego nie chcę rozgałęzienia. Wiem, co to jest rozgałęzienie. Cieszę się, że uczysz dzieci rozgałęziania, ale chciałbym się dowiedzieć o operatorach boolowskich (i operacjach bitowych, ale jestem pewien, że są w porządku)

Odpowiedzi:


42

To, co powoduje rozgałęzienie w GLSL, zależy od modelu GPU i wersji sterownika OpenGL.

Większość procesorów graficznych wydaje się mieć formę operacji „wybierz jedną z dwóch wartości”, która nie wiąże się z żadnymi kosztami rozgałęzienia:

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

a czasem nawet takie rzeczy jak:

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

zostanie zredukowany do kilku operacji wyboru wartości bez kary za rozgałęzienie.

Niektóre karty graficzne / sterowniki mają (miały?) Trochę kary na operatorze porównania między dwiema wartościami, ale szybszą operację w porównaniu do zera.

Gdzie może to być szybsze:

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

zamiast porównywać (tmp1 != tmp2)bezpośrednio, ale jest to bardzo zależne od procesora graficznego i sterownika, więc chyba, że ​​celujesz w bardzo konkretny procesor graficzny i nie ma innych, zalecam użycie operacji porównania i pozostaw to zadanie optymalizacji sterownikowi OpenGL, ponieważ inny sterownik może mieć problem z dłuższą postacią i bądź szybszy dzięki prostszemu, bardziej czytelnemu sposobowi.

„Gałęzie” też nie zawsze są złe. Na przykład na GPU SGX530 używanym w OpenPandora ten 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;
    }

Skończyło się znacznie szybciej niż ten równoważny moduł cieniujący (80 ms):

    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;

Nigdy nie wiesz z góry, jak będzie działał określony kompilator GLSL lub konkretny procesor graficzny, dopóki go nie przetestujesz.


Aby dodać punkt (nawet jeśli nie mam rzeczywistych numerów taktowania i kodu modułu cieniującego, aby przedstawić ci tę część), obecnie używam jako mojego zwykłego sprzętu testowego:

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

Jako szeroka gama różnych, popularnych modeli GPU do testowania.

Testowanie każdego ze sterownikami OpenGL i OpenCL dla systemów Windows, Linux i Linux typu open source.

I za każdym razem, gdy próbuję mikrooptymalizować moduł cieniujący GLSL (jak w powyższym przykładzie SGX530) lub operacje OpenCL dla jednej konkretnej kombinacji GPU / sterownika, kończę równie negatywnie na wydajności na więcej niż jednym z innych GPU / sterowników.

Tak więc poza wyraźnym zmniejszeniem złożoności matematycznej wysokiego poziomu (np. Konwersja 5 identycznych działów na jedną odwrotność i 5 multiplikacji zamiast) i zmniejszenie wyszukiwania tekstur / przepustowości, najprawdopodobniej będzie to strata czasu.

Każda karta graficzna jest zbyt inna od pozostałych.

Jeśli pracowałbyś konkretnie na (a) konsolach do gier z konkretnym GPU, byłaby to inna historia.

Innym (mniej znaczącym dla małych twórców gier, ale wciąż godnym uwagi) aspektem jest to, że komputerowe sterowniki GPU mogą pewnego dnia cicho zastąpić shadery ( jeśli twoja gra stanie się wystarczająco popularna ) niestandardowymi, ponownie napisanymi, zoptymalizowanymi dla tego konkretnego GPU. Robiąc to wszystko działa dla ciebie.

Zrobią to w przypadku popularnych gier, które są często używane jako punkty odniesienia.

Lub jeśli dasz swoim graczom dostęp do shaderów, aby mogli je z łatwością edytować, niektórzy z nich mogą wycisnąć kilka dodatkowych FPS na swoją korzyść.

Na przykład istnieją stworzone przez fanów pakiety cieniowania i tekstur dla Oblivion, aby radykalnie zwiększyć liczbę klatek na skądinąd trudnym do grania sprzęcie.

I wreszcie, gdy twój moduł cieniujący stanie się wystarczająco skomplikowany, gra jest prawie ukończona i zaczniesz testować na innym sprzęcie, będziesz wystarczająco zajęty, po prostu ustawiając swoje moduły cieniujące do pracy na różnych procesorach graficznych, ponieważ jest to spowodowane różnymi błędami, których nie chcesz mieć czas na ich optymalizację do tego stopnia.


„Lub jeśli dasz swoim graczom dostęp do shaderów, aby mogli z łatwością edytować je sami ...” Skoro już o tym wspomniałeś, jakie może być twoje podejście do shaderów do wallhack i tym podobnych? System honorowy, zweryfikowany, raporty ...? Podoba mi się idea lobby ograniczonego do tych samych shaderów / zasobów, niezależnie od tego, jakie mogą być, ponieważ postawy dotyczące maksymalnego / minimalnego / skalowalnego realizmu, exploitów itp. Powinny zbliżać graczy i modderów, aby zachęcać do przeglądu, współpracy itp. Wydaje mi się, że pamiętam, że tak działał Mod Gary'ego, ale jestem poza pętlą.
John P

1
@JohnP Jeśli chodzi o bezpieczeństwo, wszystko, co zakłada, że ​​klient nie jest zagrożony, i tak nie działa. Oczywiście, jeśli nie chcesz, aby ludzie edytowali swoje shadery, nie ma sensu ich ujawniać, ale tak naprawdę nie pomaga to zbytnio z bezpieczeństwem. Twoja strategia wykrywania rzeczy takich jak ataki na ścianę powinna traktować bałagan po stronie klienta jako niską pierwszą barierę i zapewne może być większa korzyść z umożliwienia lekkiego modowania, ponieważ w tej odpowiedzi, jeśli nie prowadzi to do wykrycia nieuczciwej przewagi dla gracza .
Cubic

8
@JohnP Jeśli nie chcesz, aby gracze również widzieli przez ściany, nie pozwól serwerowi przesyłać im żadnych informacji o tym, co jest za ścianą.
Polygnome

1
Tylko tyle - nie jestem przeciwny hakowaniu ścian między graczami, którzy lubią to z jakiegokolwiek powodu. Jako gracz zrezygnowałem jednak z kilku tytułów AAA, ponieważ - między innymi - stworzyli przykłady estetycznych modderów podczas pieniędzy / XP / etc. hakerzy pozostali nietknięci (którzy zarabiali naprawdę na tych sfrustrowanych, którzy byli wystarczająco sfrustrowani, aby zapłacić), mieli za mało personelu i zautomatyzowali system zgłaszania i odwoływania się, a także upewnili się, że gry żyją i umierają z powodu liczby serwerów, które chcieli utrzymać przy życiu. Miałem nadzieję, że może być bardziej zdecentralizowane podejście zarówno jako twórcy, jak i gracza.
John P

Nie, nie robię inline, jeśli gdziekolwiek. I just float (boolean statement) * (coś)
Geklmintendon't z Awesome

7

Odpowiedź Stephana Hockenhulla prawie daje ci to, co musisz wiedzieć, będzie całkowicie zależne od sprzętu.

Ale pozwól, że podam kilka przykładów tego, jak może być zależny od sprzętu i dlaczego rozgałęzienie jest w ogóle problemem, co GPU robi za kulisami, kiedy rozgałęzienie ma miejsce.

Skupiam się przede wszystkim na Nvidii, mam pewne doświadczenie z programowaniem CUDA na niskim poziomie i widzę, co generuje PTX ( IR dla jąder CUDA , takich jak SPIR-V, ale tylko dla Nvidii) i widzę standardy wprowadzania pewnych zmian.

Dlaczego rozgałęzienie w architekturze GPU jest tak ważną sprawą?

Dlaczego rozgałęzienie jest złe? Dlaczego procesory graficzne starają się przede wszystkim unikać rozgałęzień? Ponieważ procesory graficzne zwykle używają schematu, w którym wątki mają ten sam wskaźnik instrukcji . Procesory graficzne wykorzystują architekturę SIMDtypowo i chociaż szczegółowość tego może się zmienić (tj. 32 wątki dla Nvidii, 64 dla AMD i innych), na pewnym poziomie grupa wątków ma ten sam wskaźnik instrukcji. Oznacza to, że wątki te muszą patrzeć na ten sam wiersz kodu, aby wspólnie pracować nad tym samym problemem. Możesz zapytać, w jaki sposób mogą korzystać z tych samych wierszy kodu i wykonywać różne czynności? Używają różnych wartości w rejestrach, ale rejestry te są nadal używane w tych samych wierszach kodu w całej grupie. Co się stanie, gdy przestanie to mieć miejsce? (IE gałąź?) Jeśli program naprawdę nie ma możliwości obejścia tego problemu, dzieli grupę (Nvidia, takie pakiety 32 wątków są nazywane Warp , dla AMD i akademii obliczeń równoległych, jest to nazywane frontem falowym) w dwóch lub więcej różnych grupach.

Jeśli są tylko dwa różne wiersze kodu, na których byś skończył, wówczas działające wątki są podzielone na dwie grupy (od tego miejsca nazywam je wypaczeniami). Załóżmy, że architektura Nvidii, w której rozmiar wypaczenia wynosi 32, jeśli połowa tych wątków się rozejdzie, wtedy będziesz mieć 2 wypaczenia zajęte przez 32 aktywne wątki, co sprawia, że ​​rzeczy są o połowę mniej wydajne od obliczeniowego do końca. Na wielu architekturach GPU będzie próbowała temu zaradzić poprzez konwergencję wątków z powrotem w jedną warp po osiągnięciu tego samego rozgałęzienia instrukcji, lub kompilator wyraźnie umieści punkt synchronizacji, który mówi GPU, aby zjednoczył wątki lub spróbuje.

na przykład:

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

Wątek ma duży potencjał do rozbieżności (odmiennych ścieżek instrukcji), więc w takim przypadku może dojść do zbieżności, w r += t;której wskaźniki instrukcji byłyby znowu takie same. Rozbieżności mogą również wystąpić w przypadku więcej niż dwóch gałęzi, co powoduje jeszcze mniejsze wykorzystanie osnowy, cztery gałęzie oznaczają, że 32 wątki zostaną podzielone na 4 osnowy, wykorzystanie przepustowości 25%. Konwergencja może jednak ukryć niektóre z tych problemów, ponieważ 25% nie utrzymuje przepustowości w całym programie.

W mniej skomplikowanych procesorach graficznych mogą wystąpić inne problemy. Zamiast rozbieżności obliczają jedynie wszystkie gałęzie, a następnie wybierają dane wyjściowe na końcu. Może to wyglądać tak samo jak rozbieżność (oba mają wykorzystanie przepustowości 1 / n), ale istnieje kilka poważnych problemów z podejściem duplikacji.

Jednym z nich jest zużycie energii, zużywasz znacznie więcej energii, gdy tylko zdarzy się gałąź, byłoby to złe dla mobilnego gpus. Po drugie, rozbieżność zdarza się tylko na Nvidii gpus, gdy wątki tej samej osnowy podążają różnymi ścieżkami, a tym samym mają inny wskaźnik instrukcji (który jest wspólny jak pascal). Możesz więc nadal mieć rozgałęzienia i nie mieć problemów z przepustowością procesorów graficznych Nvidia, jeśli występują one w wielokrotnościach 32 lub występują tylko w jednej warstwie z kilkudziesięciu. jeśli gałąź może się zdarzyć, jest bardziej prawdopodobne, że mniej wątków się rozejdzie i i tak nie będziesz mieć problemu z rozgałęzianiem.

Innym mniejszym problemem jest to, że porównując procesory graficzne z procesorami, często nie mają one mechanizmów przewidywania i innych solidnych mechanizmów rozgałęzionych ze względu na to, ile sprzętu zajmują te mechanizmy, z tego powodu często nie widać wypełnienia nowoczesnych GPU.

Praktyczny przykład architektonicznej różnicy GPU

Teraz weźmy przykład Stephanesa i zobaczmy, jak wyglądałby zespół bezrozdziałowych rozwiązań na dwóch teoretycznych architekturach.

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

Jak powiedział Stephane, kiedy kompilator urządzeń napotka gałąź, może zdecydować o użyciu instrukcji, aby „wybrać” element, który ostatecznie nie miałby kary za gałąź. Oznacza to, że na niektórych urządzeniach można to skompilować do czegoś podobnego

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

na innych bez instrukcji wyboru, można ją skompilować

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

który może wyglądać następująco:

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

który jest bezgałęziowy i równoważny, ale przyjmuje znacznie więcej instrukcji. Ponieważ przykład Stephanesa zostanie najprawdopodobniej skompilowany na dowolnym z tych systemów, nie ma większego sensu próby samodzielnego obliczenia matematyki w celu samodzielnego usunięcia rozgałęzień, ponieważ kompilator pierwszej architektury może zdecydować się na kompilację do drugiej postaci zamiast szybsza forma.


5

Zgadzam się ze wszystkim, co zostało powiedziane w odpowiedzi @Stephane Hockenhull. Aby rozwinąć ostatni punkt:

Nigdy nie wiesz z góry, jak będzie działał określony kompilator GLSL lub konkretny procesor graficzny, dopóki go nie przetestujesz.

Absolutnie prawdziwe. Co więcej, widzę, że tego rodzaju pytania pojawiają się dość często. Ale w praktyce rzadko widywałem shader fragmentów jako źródło problemu z wydajnością. O wiele bardziej powszechne jest to, że inne czynniki powodują problemy, takie jak zbyt wiele odczytów stanu z GPU, zamiana zbyt wielu buforów, zbyt wiele pracy w jednym wywołaniu losowania itp.

Innymi słowy, zanim zaczniesz martwić się mikrooptymalizacją modułu cieniującego, profiluj całą aplikację i upewnij się, że moduły cieniujące powodują spowolnienie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.