Operacje filtrowania obrazu, takie jak rozmycie, SSAO, kwitnienie itd., Są zwykle wykonywane przy użyciu programów cieniujących piksele i operacji „gromadzenia”, przy czym każde wywołanie modułu cieniującego pikseli powoduje pobranie szeregu tekstur w celu uzyskania dostępu do wartości sąsiednich pikseli i oblicza wartość pojedynczego piksela wynik. Podejście to ma teoretyczną nieskuteczność, ponieważ wykonuje się wiele zbędnych pobrań: pobliskie wywołania modułu cieniującego ponownie pobiorą wiele takich samych tekstur.
Innym sposobem na to jest zastosowanie shaderów obliczeniowych. Mają one tę potencjalną zaletę, że mogą dzielić niewielką ilość pamięci w grupie wywołań modułu cieniującego. Na przykład, możesz kazać każdemu wywołaniu pobrać jeden tekst i zapisać go we wspólnej pamięci, a następnie obliczyć wyniki z tego miejsca. To może, ale nie musi być szybsze.
Pytanie brzmi, w jakich okolicznościach (jeśli w ogóle) metoda obliczania-cieniowania jest rzeczywiście szybsza niż metoda cieniowania pikseli? Czy zależy to od wielkości jądra, rodzaju operacji filtrowania itp.? Oczywiście odpowiedź będzie się różnić w zależności od modelu GPU, ale interesuje mnie to, czy istnieją jakieś ogólne trendy.
źródło
Odpowiedzi:
Architektoniczną zaletą shaderów obliczeniowych do przetwarzania obrazu jest to, że pomijają one etap ROP . Jest bardzo prawdopodobne, że zapisy z shaderów pikseli przechodzą przez cały regularny sprzęt do miksowania, nawet jeśli go nie używasz. Ogólnie rzecz biorąc, shadery obliczeniowe przechodzą inną (i często bardziej bezpośrednią) ścieżką do pamięci, dzięki czemu można uniknąć wąskiego gardła, które w przeciwnym razie byś miał. Słyszałem o dość znacznych wygranych w wydajności.
Architektoniczną wadą shaderów obliczeniowych jest to, że GPU nie wie już, które elementy pracy przechodzą na emeryturę i do których pikseli. Jeśli korzystasz z potoku cieniowania pikseli, GPU ma możliwość spakowania pracy w warp / wavefront, które zapisują w obszarze celu renderowania, który jest przyległy w pamięci (który może być sąsiadująco w kolejności Z lub coś podobnego dla wydajności powody). Jeśli korzystasz z potoku obliczeniowego, procesor graficzny może nie kopać pracy w optymalnych partiach, co prowadzi do większego wykorzystania przepustowości.
Być może będziesz w stanie zmienić to zmienione pakowanie w warp / wavefront na korzyść, jeśli wiesz, że Twoja konkretna operacja ma podbudowę, którą możesz wykorzystać, pakując powiązane prace do tej samej grupy wątków. Tak jak powiedziałeś, teoretycznie możesz przerwać sprzęt do próbkowania, próbkując jedną wartość na linię i umieszczając wynik w pamięci współużytkowanej dla grup, aby uzyskać dostęp do innych linii bez próbkowania. To, czy wygrasz, zależy od tego, ile kosztuje pamięć współdzielona przez grupę: jeśli jest tańsza niż pamięć podręczna tekstur najniższego poziomu, może to być wygrana, ale nie ma na to żadnej gwarancji. Procesory graficzne już całkiem dobrze radzą sobie z bardzo lokalnymi pobieraniami tekstur (z konieczności).
Jeśli masz pośrednie etapy operacji, w których chcesz udostępnić wyniki, bardziej sensowne może być użycie pamięci współdzielonej przez grupę (ponieważ nie możesz polegać na sprzęcie do próbkowania tekstur bez faktycznego zapisania wyniku pośredniego w pamięci). Niestety nie możesz również polegać na wynikach z jakiejkolwiek innej grupy wątków, więc drugi etap musiałby ograniczyć się tylko do tego, co jest dostępne w tym samym kafelku. Myślę, że kanonicznym przykładem tutaj jest obliczenie średniej luminancji ekranu dla automatycznej ekspozycji. Mógłbym również wyobrazić sobie połączenie upsamplowania tekstur z jakąś inną operacją (ponieważ upsampling, inaczej niż próbkowanie w dół i rozmycie, nie zależy od żadnych wartości poza danym kafelkiem).
źródło
John napisał już świetną odpowiedź, więc zastanów się nad jej przedłużeniem.
Obecnie dużo pracuję z modułami obliczeniowymi dla różnych algorytmów. Ogólnie rzecz biorąc, odkryłem, że shadery obliczeniowe mogą być znacznie szybsze niż ich równoważne shadery pikseli lub alternatywy oparte na sprzężeniu zwrotnym.
Gdy obejrzysz się, jak działają shadery obliczeniowe, w wielu przypadkach mają one również większy sens. Użycie modułu cieniującego piksele do filtrowania obrazu wymaga skonfigurowania bufora ramki, wysyłania wierzchołków, korzystania z wielu stopni modułu cieniującego itp. Dlaczego powinno to być wymagane do filtrowania obrazu? Przyzwyczajenie się do renderowania pełnoekranowych quadów do przetwarzania obrazów jest z pewnością jedynym „ważnym” powodem, aby nadal je używać. Jestem przekonany, że nowicjusz w dziedzinie grafiki obliczeniowej uznałby, że shadery obliczeniowe są bardziej naturalne do przetwarzania obrazu niż renderowania tekstur.
Twoje pytanie dotyczy w szczególności filtrowania obrazów, więc nie będę zbyt szczegółowo omawiał innych tematów. W niektórych naszych testach po prostu skonfigurowanie sprzężenia zwrotnego transformacji lub przełączenie obiektów bufora ramki w celu renderowania na teksturę może spowodować koszty wydajności około 0,2 ms. Pamiętaj, że wyklucza to wszelkie renderowanie! W jednym przypadku zachowaliśmy dokładnie ten sam algorytm przeniesiony do obliczeń shaderów i zauważyliśmy zauważalny wzrost wydajności.
Podczas korzystania z shaderów obliczeniowych można wykorzystać więcej krzemu na GPU do wykonania rzeczywistej pracy. Wszystkie te dodatkowe kroki są wymagane podczas korzystania z trasy modułu cieniującego piksele:
Można argumentować, że inteligentne sterowniki mogą negować wszystkie wspomniane wcześniej zalety wydajności. Miałbyś rację. Taki sterownik może stwierdzić, że renderujesz quad pełnoekranowy bez testowania głębokości itp., I skonfigurować „szybką ścieżkę”, która pomija wszelkie bezużyteczne prace obsługiwane w celu obsługi modułów cieniujących piksele. Nie zdziwiłbym się, gdyby niektórzy kierowcy zrobili to w celu przyspieszenia przetwarzania końcowego w niektórych grach AAA dla ich konkretnych układów GPU. Możesz oczywiście zapomnieć o takim leczeniu, jeśli nie pracujesz nad grą AAA.
Sterownik nie może jednak znaleźć lepszych możliwości równoległości oferowanych przez potok shadera obliczeniowego. Weź klasyczny przykład filtra gaussowskiego. Korzystając z shaderów obliczeniowych, możesz zrobić coś takiego (oddzielając filtr lub nie):
Krok 1 jest tutaj kluczem. W wersji cieniowania pikseli obraz źródłowy jest próbkowany wiele razy na piksel. W wersji modułu obliczeniowego każdy tekst źródłowy jest odczytywany tylko raz w grupie roboczej. Odczyty tekstur zwykle używają pamięci podręcznej opartej na kafelkach, ale ta pamięć podręczna jest nadal znacznie wolniejsza niż pamięć współdzielona.
Filtr gaussowski jest jednym z prostszych przykładów. Inne algorytmy filtrowania oferują inne możliwości udostępniania wyników pośrednich wewnątrz grup roboczych przy użyciu pamięci współdzielonej.
Jest jednak pewien haczyk. Shadery obliczeniowe wymagają wyraźnych barier pamięci, aby zsynchronizować swoje dane wyjściowe. Istnieje również mniej zabezpieczeń chroniących przed błędnym dostępem do pamięci. Dla programistów z dobrą znajomością programowania równoległego shadery obliczeniowe oferują znacznie większą elastyczność. Ta elastyczność oznacza jednak, że łatwiej jest również traktować shadery obliczeniowe jak zwykły kod C ++ i pisać wolny lub niepoprawny kod.
Referencje
źródło
Natknąłem się na tego bloga: Compute Shader Optimizations for AMD
Biorąc pod uwagę, jakie sztuczki można wykonać w module obliczeniowym (które są specyficzne tylko dla modułów obliczeniowych) byłem ciekawy, czy równoległa redukcja w module obliczeniowym była szybsza niż w przypadku modułu cieniującego piksele. Wysłałem e-mail do autora, Wolfa Engela, aby zapytać, czy próbował shadera pikseli. Odpowiedział, że tak i wstecz, gdy pisał post na blogu, wersja modułu obliczeniowego była znacznie szybsza niż wersja modułu cieniującego piksele. Dodał również, że dziś różnice są jeszcze większe. Najwyraźniej zdarzają się przypadki, w których korzystanie z modułu obliczania może być wielką zaletą.
źródło