Programowanie współbieżne, Języki programowania, Aplikacje, Gry

CPU vs GPU: Architektura cz. 1

Listopad 17th, 2010 Marcin Borowiec

Tym postem zaczynam serię artykułów, w których porównam architekturę aktualnie dostępnych układów CPU od Intela i AMD, układów GPU firmy NVidia montowanych w kartach GeForce począwszy na serii 8xxx i skończywszy na najnowszej 5xx, układów GPU firmy AMD/ATI montowanych w kartach Radeon począwszy od serii HD 4xxx i skończywszy na serii HD 6xxx, a także układów z serii Knights od Intela.

Ponieważ temat jest bardzo rozległy musiałem go podzielić na kilka części:

  1. Opisanie różnych typów jednostek obliczeniowych pod kątem „niezależności” wykonywania różnych ścieżek programu naraz czy też możliwości wykonywania operacji na wielu różnych porcjach danych w „jednym momencie”.
  2. Porównanie układów pod kątem typów jednostek obliczeniowych  w nich występujących (a raczej hierarchii tych jednostek)
  3. Opisanie sposobów wymiany danych i synchronizacji pomiędzy jednostkami. Opisanie dostępu do pamięci a także rozjaśnienie terminów: superskalarność, dual-issue
  4. Powiązanie informacji wymienionych w pkt3. z konkretnymi układami.
  5. Podanie przykładów algorytmów i sposoby ich implementacji dla różnych układów.

Jakiś czas temu porównałem ze sobą różne układy gpu i cpu pod względem teoretycznej maksymalnej wydajności. W tym i następnym poście postaram się już częściowo odpowiedzieć, które układy potrafią wykorzystać w większym stopniu swoją moc obliczeniową, a które układy będą mieć „puste GFLOPSy”.  Sytuacje można w dużym stopniu porównać do procesorów Pentium 4, które były pokonywane przez dużo wolniejsze (taktowane wolniejszym zegarem) Athlony czy Pentium M (które to później ewoluowały w Core 2 Duo). Wtedy mówiliśmy o „pustych gigahercach”

Najnowsze układy CPU i GPU są układami wielordzeniowymi. Zwykłe procesory w komputerach osobistych mają już po 2,3,4 a nawet 6 rdzeni. Co więcej każdy z tych rdzeni zawiera możliwość wykonania obliczeń na kilku danych jednocześnie, między innymi dzięki instrukcjom SSE. Możliwość wykonania kilka instrukcji w jednym cyklu uzyskano także dzięki superskalarności czy funkcji HyperThreading. Jeszcze bardziej ciekawie wygląda sprawa z układami kart graficznych. W najnowszych modelach mamy dostępne już kilkaset małych rdzeni. Te 6 rdzeni w głównym procesorze komputera nie robi przy tym wrażenia. Oczywiście nie dokonano tutaj jakiegoś cudu, te „procesorki” są po prostu dużo bardziej prymitywne. Aby je porównać posłużę się poniższym rysunkiem:

Te skróty na rysunku to sposoby na zwielokrotnienie jednostek obliczeniowych w procesorach  i oznaczają:

  • SIMD (Single Instruction, Multiple Data) – Najprostsze wprowadzenie dodatkowych jednostek obliczeniowych do procesora. Każda jednostka w jednej grupie SIMD wykonuje tą samą operacje na różnych danych. Najlepszym przykładem SIMD są instrukcje SSE w procesorach z rodziny x86. Jednostki SIMD idealnie nadają się do prostych operacji np przetwarzania obrazu. Przykładowo aby rozjaśnić obraz wystarczy dodać do wartości koloru kolejnych pikseli stały kolor. Zastępując jedną skalarną jednostkę obliczeniową jedną grupą SIMD składającą się z 4 jednostek otrzymamy bardzo niskim kosztem 4-krotne przyspieszenie. Aby zwiększyć użyteczność jednostek SIMD stworzono wariacje podstawowych instrukcji, np w przypadku dodawania mamy dodawanie z nasyceniem i dodawanie z przepełnieniem. Jeśli chcemy rozjaśnić obraz to interesuje nas dodawanie z nasyceniem. Gdy operujemy na jednostkach skalarnych możemy zawsze użyć dodawania z przepełnieniem + osobną instrukcję warunkową ustawiającą kolor biały w przypadku przepełnienia. Jednostki w jednej grupie SIMD nie mają własnego kontekstu wątku co powoduje, że albo wszystkie jednostki wykonają daną instrukcję albo żadna. Przez co jednej instrukcji dodawania  z nasyceniem nie da się zastąpić dodawaniem z przepełnieniem + instrukcja warunkowa(Przypadek gdy musimy ustawić kolor biały tylko dla części danych). Oczywiście nie da się mnożyć podstawowych instrukcji w nieskończoność i w wielu przypadkach gdy mamy dużo ‚ifów” zależnych od konkretnej wartości, na której operujemy SIMD staje się bezużyteczne.
  • VLIW (Very Long Instruction Word) – Rozszerza SIMD o możliwość wykonania różnych instrukcji dla każdej jednostki wchodzącej w skład grupy VLIW. Nadal jednak to cała grupa posiada jeden kontekst wątku. VLIW ma troszkę inne zadanie niż SIMD. SIMD używamy tam gdzie musimy wykonać tą samą sekwencję operacji na zbiorze danych. VLIW ma za zadanie przyspieszyć wykonanie jednego wątku poprzez zrównoleglenie instrukcji, które od siebie nie zależą, np taką sekwencję:
    1) d = a + b;
    2) e = a * c;
    3) f = c + d;
    można wykonać w ten sposób:
    1)d = a + b;          e = a * c;
    2)f = c + d;
    Mówiąc krótko, wyniki operacji pierwszej i drugiej nie zależą od siebie więc instrukcje mogą zostać wykonane jednocześnie.
  • SIMT (Single Instruction, Multiple Thread) – Termin zaproponowany przez NVidię przy okazji premiery pierwszy kart graficznych z obsługą CUDA. SIMT jest rozszerzeniem SIMD poprzez stworzenie kontekstu wątku dla każdego procesora wchodzącego w skład grupy SIMT. Teraz „ify” zależne od wartości ze zbioru danych, na którym operujemy nie są już takie straszne. W czasie wykonywania instrukcji warunkowej procesor w zależności od aktualnie wykonywanej gałęzi (if albo else) i swojego kontekstu wątku wykonuje daną operacje albo zostaje tymczasowo wyłączony. W przypadku pętli każdy z procesorów wyłącza się po wykonaniu swoich iteracji i czeka aż ostatni z grupy SIMT zakończy pętle. Dopiero wtedy cała grupa SIMT kontynuuje obliczenia.
  • SIMF (Single Instruction, Multiple Flow) – Jest to termin wymyślony przeze mnie na potrzeby tego artykułu. SIMF w porównaniu do SIMT pozwala każdemu procesorowi na niezależne wykonanie sekwencji instrukcji (choć ciągle mówimy o tej samej sekwencji instrukcji dla każdego procesora wchodzącego w skład grupy SIMF). Różnica polega na tym, że każdy z procesorów może przetwarzać inną gałąź tej samej sekwencji. Tzn jeden procesor może wykonywać właśnie instrukcje dla spełnionego warunku „if”, inny może w tym czasie wykonywać kod dla else. W przypadku pętli procesor nie czeka na wyjście z pętli innych procesorów.
  • MIMD (Multiple Instruction, Multiple Data) – Oznacza pełną swobodę. Każdy z procesorów może wykonywać całkiem inny kod na całkiem innych danych. Procesor w grupie MIMD to przede wszystkim popularny rdzeń w procesorze komputera (CPU). Granica pomiędzy SIMF a MIMD jest trochę zamazana. Teoretycznie możemy przecież napisać kod w postaci if (warunek()) Funkcja1() else Funkcja2(). Mamy jedną sekwencje instrukcji, która zawiera dwie gałęzie zależne od warunku. W przypadku SIMF każdy procesor niezależnie wykona gałąź odpowiednią dla niego. Więc czym tak na prawdę różni się SIMF od MIMD? Wyjaśnię to dokładnie w kolejnym poście.

Patrząc na powyższy rysunek można powiedzieć, że sposoby znajdujące się po lewej stronie oznaczają niski koszt zwielokrotnienia jednostek obliczeniowych i dużo ograniczeń co do sposobu zrównoleglenia obliczeń. Im bardziej na prawo tym mamy większą swobodę i możliwości lecz koszt wzrasta. Które z rozwiązań jest najlepsze? To zależy od zastosowania. Niski koszt w przypadku SIMD, VLIW powoduje że możemy dodać dużo dodatkowych jednostek jednak gdy okaże się że nasze obliczenia ciężko zrównoleglić, lepszym rozwiązaniem może być np stworzenie 2 procesorów MIMD zamiast np 16 SIMD. Ważna jest jeszcze jedna rzecz. Nikt nie powiedział, że w danym procesorze możemy mieć tylko jedną grupę jednostek obliczeniowych. Można np. stworzyć układ który będzie zawierał grupę MIMD zawierają grupę procesorów SIMT. Właśnie takie drzewiaste rozwiązania stosuje się w procesorach CPU i GPU. Rysunek pokazuje jeszcze jedną rzecz. Mamy dwie drogi w „zrównoleglaniu” programu. Pierwsze ze względu na dane: SIMD -> SIMT -> SIMF -> MIMD gdzie kolejny skrót oznacza coraz bardziej elastyczny przypadek oraz ze względu na kod VLIW -> MIMD gdzie VLIW oznacza wykonywanie różnych operacji w ramach jednego wątku a MIMD wykonywanie różnych zadań w ramach jednego programu.

Ciąg dalszy w następnym poście.