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.
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:
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.
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
na innych bez instrukcji wyboru, można ją skompilować
który może wyglądać następująco:
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.
źródło
Zgadzam się ze wszystkim, co zostało powiedziane w odpowiedzi @Stephane Hockenhull. Aby rozwinąć ostatni punkt:
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.
źródło