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.