Projekt okładki i stron tytułowych

Grzegorz Laskowski

Ilustracja na okładce

Ociacia/Shutterstock

Wydawca

Łukasz Łopuszański

Redaktor

Henryka Walas

Koordynator produkcji

Anna Bączkowska

Skład wersji elektronicznej na zlecenie Wydawnictwa Naukowego PWN

Magdalena Wojtas / 88em.eu

Książka, którą nabyłeś, jest dziełem twórcy i wydawcy. Prosimy, abyś przestrzegał praw, jakie im przysługują. Jej zawartość możesz udostępnić nieodpłatnie osobom bliskim lub osobiście znanym. Ale nie publikuj jej w internecie. Jeśli cytujesz jej fragmenty, nie zmieniaj ich treści i koniecznie zaznacz, czyje to dzieło. A kopiując jej część, rób to jedynie na użytek osobisty.

Szanujmy cudzą własność i prawo

Więcej na www.legalnakultura.pl

Polska Izba Książki

Copyright © by Wydawnictwo Naukowe PWN SA

Warszawa 2014

eBook został przygotowany na podstawie wydania papierowego z 2014 r.

(wyd. I)

Warszawa 2016

ISBN 978-83-01-18045-4 (ePub)

ISBN 978-83-01-18046-1 (Mobi)

Wydawnictwo Naukowe PWN SA

02-460 Warszawa, ul. Gottlieba Daimlera 2

tel. 22 69 54 321, faks 22 69 54 288

infolinia 801 33 33 88

e-mail: pwn@pwn.com.pl

www.pwn.pl

Spis treści

1. Wprowadzenie

2. Standard OpenCL

2.1. Model platformy

2.2. Model pamięci

2.3. Model wykonawczy

2.3.1. Siatka obliczeniowa – NDRange

2.3.2. Kontekst obliczeń oraz kolejka poleceń

2.4. Model programowania

3. Język programowania dostępny w OpenCL

3.1. Typy danych

3.1.2. Typy wektorowe

3.1.3. Typy do obsługi obrazów

3.1.4. Inne typy oraz nazwy zarezerwowane

3.2. Dostępne operatory

3.3. Funkcje wbudowane

3.4. Dodatkowe kwalifikatory oraz atrybuty

3.5. Operacje na typach wektorowych OpenCL

3.5.1. Dane typu wektorowego – dostęp do składowych

3.5.2. Operacje na typach wektorowych

3.5.3. Funkcje obsługujące dane typu wektorowego

3.6. Obsługa siatki obliczeniowej

3.7. Funkcje synchronizacji i funkcje atomowe

3.8. Funkcje do obsługi obrazów

3.9. Preprocesor OpenCL C

3.10. Ograniczenia OpenCL C

4. OpenCL – łatwe przykłady na początek

4.1. Informacje o urządzeniu obliczeniowym

4.1.1. Wersja dla API w języku C

4.1.2. Informacje o urządzeniu obliczeniowym obiektowo i w C++

4.2. Witaj Świecie!

4.2.1. Przygotowania

4.2.2. Uruchomienie jądra obliczeniowego

4.3. Dodawanie wektorów

4.3.1. Jądro obliczeniowe

4.3.2. Wersja dla C++

4.3.3. Inne operacje na wektorach do ćwiczeń

4.4. Siatka lokalna oraz globalna

4.5. Zlecanie i wykonywanie zadań

4.6. Uwagi o kompilacji jądra obliczeniowego

5. Przykłady obliczeń w OpenCL

5.1. Obsługa printf w OpenCL

5.2. Operacja redukcji

5.2.1. Operacja redukcji – wersja szeregowa

5.2.2. Operacja redukcji – jądro obliczeniowe podejście pierwsze

5.2.3. Operacja redukcji – jądro obliczeniowe podejście drugie

5.2.4. Operacja redukcji – jądro obliczeniowe poddane optymalizacji

5.3. Wyznaczanie histogramu

5.3.1. Wersja szeregowa

5.3.2. Wersja dla OpenCL

5.4. Równoległe szukanie prawie binarne

5.4.1. Wersja równoległa dla OpenCL

5.5. Przybliżanie wartości liczby π

5.5.1. Wersja szeregowa

5.5.2. Wersja OpenCL – podejście bezpośrednie

5.5.3. Wersja OpenCL – podejście o wyższej wydajności

5.6. Gra w życie

5.6.1. Gra w życie – procedura obliczeniowa

5.6.2. Gra w życie – obsługa symulacji

5.7. Zbiór Mandelbrota

5.7.1. Zbiór Julii

5.7.2. Tworzenie obrazu zbioru Mandelbrota – wersja szeregowa

5.7.3. Tworzenie obrazu zbioru Mandelbrota – wersja OpenCL

5.7.4. Tworzenie obrazu zbioru Julii

5.7.5. Kolorowanie zbioru fraktalnego

5.8. Algorytm sortowania bitonicznego

5.8.1. Sieci sortujące

5.8.2. Bitoniczna sieć sortująca

5.8.3. Szeregowa wersja algorytmu sortowania bitonicznego

5.8.4. Równoległa wersja algorytmu sortowania bitonicznego dla OpenCL

5.9. Przetwarzanie obrazów

5.9.1. Podstawy – wypełnienie obrazu

5.9.2. Skalowanie obrazu

5.9.3. Rozmywanie obrazu – filtr Gaussa

5.9.4. Wykrywanie krawędzi w obrazie za pomocą filtru Sobela

5.10. Współpraca OpenCL z OpenGL

5.10.1. Procedury obliczeniowe OpenCL

5.10.2. Czynności wykonywane w programie gospodarza

6. OpenCL w innych językach programowania

6.1. Język Python

6.1.1. Przegląd API pakietu PyOpenCL

6.1.2. Znów dodawanie wektorów, ale w Pythonie

6.1.3. Skrypt do testowania siatki obliczeniowej

6.1.4. Stosowanie typów użytkownika

6.1.5. Klasa w Pythonie dla łatwiejszej współpracy z OpenCL

6.2. Pakiet APARAPI dla języka Java

6.2.1. Dodawanie wektorów

6.2.2. Przegląd API pakietu APARAPI

6.2.3. Algorytm Blacka-Sholesa

6.2.4. Mechanizm rozszerzeń

6.3. Inne rozwiązania, VexCL i nie tylko

7. Zamiast zakończenia

1.1.1. Analiza wydajności symulacji przykładu „Gra w życie”

Dodatek A. Kompilacja przykładów

Dodatek B. Instalacja pakietu OpenCL dla języka Python

Dodatek C. Opis funkcji pomocniczych

C.1. Zapis do formatu PPM

C.2. Konwersja kodów formatu koloru do wartości znakowych

C.3. Zapis i odczyt danych w formacie PNG

C.4. Konwersja z modelu kolorów HSV do RGB

C.5. Utworzenie macierzy dla filtru rozmywającego

Dodatek D. Zestawianie używanych funkcji API OpenCL

D.1. Funkcja clGetPlatformIDs

D.2. Funkcja clGetPlatformInfo

D.3. Funkcja clGetDeviceInfo

D.4. Funkcja clGetSupportedImageFormats

D.5. Funkcja clGetDeviceIDs

D.6. Funkcja clCreateContext

D.7. Funkcja clBuildProgram

D.8. Funkcja clGetProgramBuildInfo

D.9. Funkcja clCreateCommandQueue

D.10. Funkcja clCreateBuffer

D.11. Funkcja clEnqueueReadBuffer

D.12. Funkcja clEnqueueWriteBuffer

D.13. Funkcja clCreateProgramWithSource

D.14. Funkcja clCreateKernel

D.15. Funkcja clSetKernelArg

D.16. Funkcja clEnqueueNDRangeKernel

D.17. Funkcja clEnqueueTask

D.18. Funkcja clGetKernelWorkGroupInfo

D.19. Funkcja clCreateImage2D

D.20. Funkcja clEnqueueReadImage

D.21. Funkcja clEnqueueWriteImage

D.22. Funkcja clFinish

D.23. Funkcja clReleaseKernel

D.24. Funkcja clReleaseProgram

D.25. Funkcja clReleaseCommandQueue

D.26. Funkcja clReleaseMemObject

D.27. Funkcja clReleaseContext

D.29. Funkcja clCreateFromGLTexture2D

D.30. Funkcja clEnqueueAcquireGLObjects

D.31. Funkcja clEnqueueReleaseGLObjects

Dodatek E. Spis kodów błędów API OpenCL

Dodatek F. Najczęściej spotykane nazwy rozszerzeń

Literatura

1   Wprowadzenie

Współczesne systemy komputerowe cechują się równoległością. Komputery klasy PC, telefony, tablety czy ogólnie urządzenia mobilne mają przynajmniej dwa procesory przeznaczone do wykonywania programów użytkownika. Karty graficzne również są wyposażone w wiele jednostek przetwarzających – karty średniej klasy mają nawet 1000 rdzeni obliczeniowych, tańsze około 300 rdzeni, a największe i najdroższe nawet 5000[1].

Techniki programowania odpowiednie dla tradycyjnego procesora z jednym rdzeniem nie dają możliwości wykorzystania mocy obliczeniowej urządzenia mającego choćby tylko 100 rdzeni obliczeniowych. Jednak powstało wiele różnych rozwiązań obsługujących programowanie równoległe.

Bardzo dużą popularność w dziedzinie uniwersalnych obliczeń wykonywanych z pomocą kart graficznych zdobyła technologia CUDA (ang. Compute Unified Device Architecture). Jednak standardowym rozwiązaniem w tej dziedzinie jest OpenCL (w skrócie standard OCL). Istotną zaletą standardu OCL jest to, że działa z kartami graficznymi różnych producentów, a także może być stosowany dla innych rozwiązań technologicznych niż karty graficzne. Warto też pamiętać o technologii C++AMP opracowanej przez Microsoft, która również jest niezależna od producentów kart graficznych, ale działa tylko w systemach operacyjnych z rodziny Windows.

Jak się wydaje, standard OpenCL będzie coraz bardziej popularny w programowaniu nie tylko kart graficznych, lecz także tradycyjnych procesorów czy innych rozwiązań o wysokiej wydajności. Nie jest on ograniczony do konkretnego systemu operacyjnego czy komputerów klasy PC, ale można go zastosować też do najnowszych układów stosowanych w telefonach i tabletach.

Duża popularność techonologii CUDA wynika również z jej daleko posuniętej integracji z językiem C/C++. W programie napisanym w technologii CUDA łatwo przeoczyć, który fragment jest wykonywany przez kartę graficzną, a który przez tradycyjny procesor. W przypadku OpenCL przygotowuje się oddzielny program, stanowiący tzw. procedurę obliczeniową (jądro obliczeniowe albo funkcję obliczeniową), a wewnętrzne mechanizmy OpenCL dopasowują podany program do aktualnie używanej platformy wykorzystywanej do obliczeń w ramach OpenCL. Oznacza to zasadniczą przewagę standardu OpenCL nad technologią CUDA, gdyż może być stosowany nie tylko do kart graficznych, ale również do tradycyjnych procesorów, które obecnie są wyposażone w wektorowe jednostki oferujące instrukcje typu SSE, AltiVec, Neon czy AVX. Nowością jest też wykorzystywanie standardu OpenCL do układów FPGA[2], które po odpowiednim zaprogramowaniu również mogą stanowić wydajną jednostkę do realizacji obliczeń.

Warto zapoznać się z technologią OpenCL, bowiem wykorzystanie kart graficznych wiodących producentów do realizacji obliczeń uniwersalnych różnego typu jest już od kilku lat bardzo popularne. Wiele aplikacji (np. związanych z przetwarzaniem grafiki, multimediów), a nawet programy do kompresji danych pozwalają na wykorzystanie mocy obliczeniowej kart graficznych do przyspieszenia obliczeń. Znakomitym przykładem jest GIMP – jeden z ogólnie dostępnych programów o otwartym źródle, a dokładnie podsystem GEGL, w którego najnowszej wersji standard OCL będzie szeroko stosowany. Innym przykładem jest program WinZIP, który wykorzystuje OpenCL do przyspieszenia kompresji oraz dekompresji.

1.1. Obliczenia równoległe

Choć obecnie istnieją dwa główne standardy tworzenia programów dla kart graficznych: wymieniona technologia CUDA firmy NVIDIA oraz standard OpenCL (który można traktować jako odpowiednik OpenGL do realizacji obliczeń). Oba rozwiązania mają swoje zalety i wady. CUDA jest bardzo popularna, jednakże działa tylko z kartami firmy NVIDIA. OpenCL to standard ustanowiony przez organizację Khronos – aplikacje opracowane za jego pomocą działają na kartach graficznych głównych producentów kart graficznych, obecnie są to firmy NVIDIA oraz AMD.

Wymienione technologie mają podobny model programowania. Zaletą technologii CUDA, jak wspomniano we wstępie, jest pełna integracja z językiem C/C++. Właściwie to należy mówić o językach programowania CUDA C i CUDA C++. OpenCL oferuje odrębny język programowania – podobny do C, ale nie jest to C++. Stanowi to też pewną wadę, ponieważ nie są dostępne np. szablony. W CUDA można tworzyć klasy języka C++, których metody są realizowane przez kartę graficzną. W przypadku OpenCL tworzone są tzw. kernele (w dalszej części książki będą one nazywane jądrami obliczeniowymi albo procedurami/funkcjami obliczeniowymi) stanowiące oddzielne programy do wykonania przez odpowiednie urządzenia obliczeniowe dostępne w ramach tzw. platformy OpenCL. Warto tu wspomnieć o pakiecie APARAPI dla języka Java (zostanie on szerzej przedstawiony w rozdz. 6.2), który funkcjonuje podobnie jak technologia CUDA. Program przeznaczony do realizacji na karcie graficznej opracowuje się w języku Java, następnie jest on tłumaczony na OpenCL, aby mógł być wykonany przez kartę graficzną (nawiasem mówiąc jest on również tłumaczony z OpenCL na odpowiedni assembler).

W obydwu wiodących technologiach można spotkać te same rozwiązania, które opierają się na podstawowych pojęciach, jakie występują w obliczeniach równoległych. Sposób programowania w pracy z jednym procesorem polega na odczytaniu zestawu danych i element po elemencie wykonaniu obliczeń. Ten sposób niestety nie sprawdzi się w przypadku, gdy mamy do dyspozycji wiele jednostek obliczeniowych, bowiem nie wykorzystamy optymalnie dostępnej mocy obliczeniowej.

Równoległe rozwiązanie problemu wymaga podzielenia zbioru na mniejsze części. Ogólnie można mówić o dwóch sposobach podziału danego zadania: pierwszy to „dziel i rządź” (ang. divide-and-conquer), stosowany także w programowaniu szeregowym. Drugi to „rozprosz i zbierz” (ang. scatter-and-gather).

W sposobie „dziel i rządź” dany problem jest dzielony na mniejsze problemy, które są łatwiejsze do rozwiązania. Odpowiednie połączenie rozwiązań mniejszych problemów pozwala rozwiązać problem główny. Drugie podejście jest podobne, ale zamiast dzielenia całego problemu na mniejsze podproblemy, rozdziela się tylko dane i przekazuje te fragmenty do jednostek obliczeniowych. Późniejszy odbiór danych oraz ich dodatkowe uporządkowanie pozwala na poprawne zakończenie obliczeń. Jak widać, w obydwu podejściach stosuje się rozdział problemu lub danych na mniejsze części.

Na rysunku 1.1 przedstawiono schemat podziału danych, a także ogólnie całego problemu na mniejsze zadania. Przedstawia on proces sortowania przez scalanie ośmiu elementów. Zestaw danych został podzielony na cztery grupy po dwa elementy. Każda z grup może być przetwarzana niezależnie od pozostałych. Następnie grupy należy połączyć ponownie. To zadanie może być realizowane niezależnie przez dwa procesory, choć ostatecznie proces sortowania może zostać zakończony przez łączenie danych wykonane przez jeden procesor.

Rys. 1.1. Sortowanie przez scalanie jako algorytm równoległy (poszczególne etapy mogą być realizowane przez kilka różnych jednostek obliczeniowych)

Pokazuje to też jeden bardzo ważny aspekt obliczeń realizowanych równolegle, a mianowicie, niezależność danych. Wysoką wydajność można otrzymać w dość prosty sposób, jeśli dane można łatwo podzielić na niezależne od siebie grupy. W przypadku sortowania przez scalanie, istotnie w pierwszym kroku można łatwo wydzielić poszczególne grupy. Jednak dalsze działania, polegające na scalaniu danych, niestety łączą je w większe grupy, co utrudnia zastosowanie przetwarzania równoległego. Mimo to, sortowanie przez scalenie jest algorytmem, który dość łatwo zaimplementować w środowisku równoległym, choć istnieje inne podejście, tzw. sortowanie bitoniczne, które w środowisku, gdzie dostępnych jest wiele jednostek przetwarzających, daje lepszą wydajność (algorytm sortowania bitonicznego został przedstawiony w rozdz. 5.8).

Innym przykładem operacji, która łatwo daje się zaimplementować w środowisku równoległym, jest mnożenie dwóch wektorów element po elemencie:

for (i=0; i<N; i++)

C[i] = A[i] * B[i];

Rezultat operacji jest umieszczany w wektorze wynikowym reprezentowanym przez zmienną C. Istotnie, poszczególne mnożenia można wykonywać w sposób zupełnie niezależny od siebie. Jednak zlecanie wykonania tylko jednej operacji mnożenia i zapisania wyniku tylko dla jednej jednostki obliczeniowej również nie jest najlepszym rozwiązaniem pod względem wydajności. Wykonanie tych operacji jest zbyt proste i dostęp do pamięci, aby odczytać kolejny element, może być bardziej czasochłonny niż wykonanie mnożenia, toteż należy tak przetwarzać dane, aby grupować je w odpowiednio duże grupy.

Ten problem w obszarze programowania równoległego nazywa się ziarnistością, (ang. grain). Podany przykład mnożenia wektorów to tzw. problem drobnoziarnisty (ang. fine-grained), bowiem mamy wiele drobnych obliczeń, które mogą być realizowane równolegle. Dodatkowo, nie ma znaczenia kolejność wykonywania poszczególnych mnożeń. Łatwo także grupować poszczególne operacje. W efekcie ułatwia to rozłożenie obciążenia obliczeniami na poszczególne jednostki obliczeniowe, co również jest bardzo istotne, aby wykorzystać moc wszystkich dostępnych jednostek.

Przeciwieństwem obliczeń drobnoziarnistych są obliczenia gruboziarniste (ang. coarse-grained), w których istnieje możliwość wskazania etapów obliczeń równoległych prowadzonych w sposób niezależny. Jednakże nie jest łatwo je rozdzielić na małe podproblemy. Trudno także zrównoważyć obciążenie poszczególnych jednostek obliczeniowych. Zazwyczaj jedną grupą danych zajmuje się jeden wątek obliczeniowy. W opisywanej technologii OpenCL jeden wątek nazywany jest też jednostką roboczą (więcej informacji można znaleźć w rozdz. 2).

Przykłady omawiane w tej książce należą do problemów drobnoziarnistych, do których rozwiązania stosunkowo łatwo można wykorzystać wiele jednostek roboczych. Większość problemów cechuje niezależność danych, ale nie wszystkie. Zdarza się nawet w przypadku obliczeń drobnoziarnistych, że wymagane jest przeprowadzenie dodatkowych operacji, aby przetwarzane dane pozostały spójne.

Dlatego w przypadku programowania równoległego potrzebne są mechanizmy synchronizacji danych zarówno na poziomie pojedynczych wątków, czy też jednostek roboczych, jak też między poszczególnymi etapami realizacji całego algorytmu. Technologia OpenCL dostarcza takie rozwiązanie na poziomie pojedynczych wątków poprzez polecenie barrier oraz na poziomie wyższym, gdzie zleca się wykonanie pewnych obliczeń przez tzw. jądra obliczeniowe. W takim przypadku należy np. zaczekać na zakończenie realizacji obliczeń, aby rozpocząć odczytywanie danych lub zlecić wykonanie kolejnej procedury obliczeniowej.

W tym krótkim wprowadzeniu nie będą dokładnie omawiane problemy programowania równoległego, w tym także współbieżnego, gdzie istotną rolę odgrywa dostęp do różnorakich zasobów. Warto jeszcze podkreślić rolę wątków w programowaniu równoległym. To zazwyczaj wątki wykonują operacje na wydzielonym zbiorze danych. Jeśli uda się tak podzielić dane, że nie ma potrzeby, aby wątki się ze sobą komunikowały, to równoległa procedura obliczeniowa ma dość prostą postać. Jednak w wielu przypadkach konieczna będzie komunikacja między wątkami. Wykonuje się ona za pomocą tzw. mechanizmu pamięci dzielonej (tego typu pamięć również jest dostępna w ramach OpenCL, nazwana jest pamięcią lokalną). W programowaniu równoległym, a dokładnie we współbieżnym, istotnym elementem jest synchronizacja danych np. za pomocą semaforów czy tzw. mutexów. W przypadku OpenCL możliwości synchronizacji wątków są dość ograniczone, bowiem wspierany jest tylko mechanizm bariery, dostępne są także tzw. instrukcje atomowe, zapewniające spójność operacji na pamięci.

Istnieją także inne sposoby realizacji aplikacji równoległych, np. przez przekazywanie komunikatów, bądź poprzez tzw. aktorów. Pojęcia te wymagałyby dokładniejszego omówienia, jednakże nie jest to celem tej ksiązki, dlatego zachęcamy w tym miejscu do zapoznania się ze spisem literatury zamieszczonym na końcu. Pozycje [Czech2013], [HerlihyShavit2010] oraz [Cormen2012] dokładniej omawiają ogólne zagadnienia związane z programowaniem równoległym.

1.2. Zawartość książki

Książek z dziedziny informatyki zazwyczaj nie czyta się od przysłowiowej deski do deski. Pozycja na temat standardu OpenCL również jest tego typu książką.

W pierwszej kolejności warto przeczytać rozdział 2, w którym znajduje się materiał wstępny opisujący architekturę OpenCL, a także rozdział 3, w którym opisano język OpenCL C. Następnie można przejść do lektury rozdziału 4, w którym pokazano mniej skomplikowane przykłady aplikacji w OpenCL. Pozwalają one zapoznać się z podstawowym API OpenCL, jakie trzeba stosować, aby wykorzystać możliwości OpenCL. Głównym językiem programowania, jaki będzie stosowany, jest język C, ale pokazano też przykłady dla języka C++.

Rozdział 5 zawiera dziesięć przykładów rozwiązań różnych zagadnień, jak np. operację redukcji, wyznaczanie histogramu, a także dobrze znany przykład rysowania zbioru fraktalnego. OpenCL oferuje też znaczne udogodnienia w przetwarzaniu obrazów – w rozdziale 5.9 podano przykłady względnie łatwego zaimplementowania kilku filtrów do szybkiego przetwarzania obrazu. Sposób współpracy OpenCL z OpenGL omówiono w rozdziale 5.10. Pokazany przykład, choć dość prosty, jednakże obrazuje zastosowanie karty graficznej do wykonywania obliczeń, a także wizualizacji za pomocą funkcji OpenGL. W niemal wszystkich rozdziałach język C/C++ stanowi główne narzędzie do sterowania obliczeniami w OpenCL, ale wyjątkiem jest rozdział 5.5, gdzie zamiast języka C/C++ stosowany jest język Python.

W rozdziale 6 omówiono wykorzystanie OpenCL w innych językach programowania. Funkcje OpenCL, wzorem OpenGL, jest stosunkowo łatwo przenieść do innego języka programowania. W rozdziale tym przedstawimy dokładniej tylko dwa rozwiązania wykorzystujące język Python oraz pakiet APARAPI współpracujący z językiem Java.

Uzupełnieniem treści książki są dodatki. W Dodatku A przedstawiono sposób kompilacji przykładów, jakie pojawiają się w całej książce. Podano też skrypt dla programu makefile, który ułatwia przeprowadzanie kompilacji np. w systemach Linux, czy MacOS, ale także funkcjonuje w rodzinie systemów Windows. Kilka informacji o tym, jak dokonać instalacji pakietu OpenCL dla języka Python podano w kolejnym dodatku.

Przykłady zaprezentowane w rozdziale 5 korzystają z kilku dodatkowych funkcji, które zostały omówione w Dodatku C. W miarę możliwości zaprezentowano też kody źródłowe tych funkcji. Istotną rolę pełni bardzo obszerny Dodatek D, gdzie w sposób syntetyczny przedstawiono wybrany zbiór funkcji API OpenCL, jaki jest używany w przykładach przedstawionych w rozdz. 4 oraz 5. Dla każdej funkcji podano także przykłady jej użycia, co odróżnia ten zestaw od podobnego, jaki można znaleźć w specyfikacji dostępnej na stronach organizacji standaryzującej technologię OpenCL.

Pomocne są dodatki E oraz F, w których zgromadzono kody błędów, jakie zwracane są przez funkcje API OpenCL, oraz spis rozszerzeń, jakie można napotkać w obecnie dostępnych implementacjach OpenCL dla najbardziej popularnych producentów kart graficznych, a także procesorów.

Ostatnim elementem jest spis literatury, która uzupełnia informacje zawarte w tej książce, oraz indeks ułatwiający odszukanie informacji o różnych funkcjach, czy pojęciach, jakie pojawiają się na stronach tej książki.

Schemat zawartości książki i relacje między poszczególnymi rozdziałami zostały pokazane na rys. 1.2. Rozdziały 1, 2, 3 oraz 4 dostarczają podstawowych informacji, dlatego warto przeczytać ich zawartość wcześniej. Natomiast kolejność lektury rozdziałów 5 i 6 może być dowolna. Warto także sięgać do dodatku D, gdzie przedstawione zostały funkcje API OpenCL, jakie pojawiają się w przykładach w rozdziałach 5 i 6, stanowiących główne rozdziały książki. Rozdział 7 nie stanowi typowego zakończenia, przedstawia bowiem sposób wykorzystania narzędzia CodeXL do testowania wydajności jednego z przykładów, jaki został przytoczony w rozdziale 5.

Rys. 1.2. Struktura rozdziałów oraz relacje między poszczególnymi rozdziałami

Przypisy

[1] Podane liczby dotyczą rozwiązań w roku 2014, z pewnością w następnych latach liczba rdzeni obliczeniowych zarówno w przypadku tradycyjnych procesorów, jak i kart graficznych zostanie podwojona.

[2] FPGA – Field Programmable Gate Array, w bezpośrednim tłumaczeniu: programowalna macierz bramek. Układ taki można wielokrotnie programować, aby zmieniać jego przeznaczenie i funkcjonalność, nawet po zamontowaniu w urządzeniu docelowym.