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

CPU vs GPU: GFLOPS

Styczeń 22nd, 2010 Marcin Borowiec

Czytając różne informacje o kartach graficznych możemy się dowiedzieć, że ich wydajność jest wielokrotnie większa od odpowiednich im cenowo procesorów x86. Często można też zobaczyć liczby w postaci nawet ponad 2TFLOPSów dla GPU i maksymalnie 100GFLOPS dla najszybszych CPU. Dzisiaj postaram się opowiedzieć o teoretycznej wydajności różnych układów i tego w jaki sposób można ją obliczyć. Natomiast w kolejnych postach opisze różnice w architekturze CPU i GPU AMD i NVidii i to jak wpływają one na poziom wykorzystania teoretycznej wydajności.

Zanim jednak przejdę do sedna sprawy muszę napisać pewne uaktualnienie do mojego poprzedniego posta o podstawach GPU. Po jego publikacji AMD wydało w końcu sterowniki (Catalyst 9.12 Hotfix) z obsługą DirectCompute 4.1 dla Radeonów 48xx i 47xx. Niestety część osób będzie zawiedziona. Nadal brakuje obsługi DirectCompute w Radeonach 46xx i słabszych, które również były często kupowane. Podobnie lista rozszerzeń OpenCL dla mojego Radeona 4850 jest ciągle pusta. Pisząc o podstawach GPU pominąłem także jedną ale do niektórych zastosowań ważną rzecz. Radeony 47xx, 48xx, 58xx posiadają obsługę obliczeń na liczbach podwójnej precyzji. NVidia z kolei podwójną precyzje posiada tylko w układach opartych na GT200 i jej implementacja jest dość wolna. Dokładniejsze liczby podam w dalszej części postu.

Na początku postu wspomniałem o FLOPSach i myślę że warto napisać najpierw co to jest. Dość dobrą definicje można przeczytać na wikipedii: http://pl.wikipedia.org/wiki/FLOPS. Jednak na powyższej stronie nie ma wyraźnie zaznaczonej jednej sprawy. Wydajność we FLOPS jednej i tej samej maszyny może dotyczyć różnych rzeczy: (i mieć w zależności od tego inną wartość)

  1. Teoretycznej wydajności wszystkich jednostek obliczeniowych opierając się przede wszystkim na operacjach dodawania i mnożenia bez takich operacji jak np. shift.
  2. Wydajności, która została uzyskana w określonym benchmarku. Taka wydajność będzie przeważnie niższa od wartości z pkt1 w związku z np czekaniem na pobranie danych z pamięci, niemożnością wykorzystania specyficznych jednostek przystosowanych tylko do pewnych operacji czy niemożliwością równoległego wykorzystania kilku jednostek z powodu zależnych od siebie operacji. Podobnie jak w poprzednim przypadku liczone są operacje dodawania i mnożenia wykonywane przez benchmark.

Opisywanie teoretycznej wydajności zaczne od dzisiejszych najmocniejszych jednostek CPU. Weźmy dla przykładu czterordzeniowy Core i7. Każdy z rdzeni posiada jedną jednostkę odpowiedzialną za dodawanie na 128bitowych danych i jedną jednostke do mnożenia na 128bitowych danych co oznacza mniej więcej maksymalnie:

  • 2 operacje x87 na rozszerzonych 80bitowych liczbach zmiennoprzecinkowych w jednym cyklu zegara
  • 4 operacje SSE na 64bitowych liczbach zmiennoprzecinkowych (podwójna precyzja) w jednym cyklu zegara
  • 8 operacji SSE na 32bitowych liczbach zmiennoprzecinkowych (pojedyncza precyzja) w jednym cyklu zegara

Co dla czterordzeniowego Core i7 o taktowaniu 3GHz oznacza wydajność odpowiednio 24GFlops, 48GFlops, 96GFlops dla danego typu operacji czyli całkiem sporo.

Dalej mamy np układ GeForce GTX 280. Do dyspozycji mamy tutaj 30 multiprocesorów, z których każdy posiada 8 jednostek SP (Shader Processor) o wydajności 2 operacje pojedynczej precyzji (jedna operacja mad czyli jedna operacja mnożenia i jedna dodawania (a*b+c)) na jeden cykl i dodatkowo 2 jednostki SFU (Special Function Unit), każda o wydajności 1 operacja specjalna (sin, log, sqrt) lub 4 operacje mnożenia na cykl (dzięki tzw. ‘dual issue’). Razem daje to 30*(8*2 + 2*4) = 720 operacji na jeden cykl dla całego GPU. Mnożąc to przez częstotliwość “szaderów” czyli 1296MHz otrzymujemy 933GFLOPS!

Niestety wydajność tego samego układu w podwójnej precyzji jest znacznie mniejsza. Każdy multiprocesor potrafi wykonać tylko jedną operacje MAD (liczoną jako dwie operacje).  Jednostki SFU nie operują w ogóle na 64bitowych liczbach zmiennoprzecinkowych. Co przekłada się na “tylko” 30*2 = 60 operacji na jeden cykl. Przy częstotliwości 1296MHz daje to ok 78GFlops.

Konkurencyjny Radeon 4870 wydany w podobnym czasie co GTX280 posiada 160 SP pogrupowanych po 16SP w jednym SIMD Engine. Każdy shader składa się z 5 jednostek SPu potrafiących wykonać jedną operacje mad na cykl (także liczone jako 2 operacje na cykl). Dodatkowo jedna z tych 5 jednostek potrafi wykonać jedną instrukcję specjalną na cykl (zamiast instrukcji mad). Razem daje to 10 * 16 * 5 * 2 = 1600 operacji na cykl czyli 1.2TFlops przy częstotliwości 750MHz. Jak widać teoretyczna wydajność Radeona 4870 jest większa niż GeForca GTX280.

W przypadku podwójnej precyzji przewaga Radeona znacznie się zwiększa, gdyż każdy SP potrafi wykonać jedną instrukcje MAD na cykl co daje 10*16*2=320 operacji na cykl co przekłada się na 240GFlops. Ponad 3x więcej niż GTX280! O ile w przypadku pojedynczej precyzji GeForce “nadrabia architekturą” i w wielu przypadkach jest szybszy od Radeona to w double precision trzeba uznać wyższość Radeona. Od razu jednak przypomnę że, aby wydobyć pełnię mocy Radeona trzeba wykorzystać niezbyt wygodny CAL.

Najnowszy Radeon 5870 z punktu widzenia dzisiejszego tematu postu jest podwojonym Radeonem 4870 (zamiast 10 SIMD Engine mamy ich 20) co oznacza 3200 operacji na cykl dla pojedynczej precyzji i 640 operacji dla podwójnej precyzji. Przy zwiększonym taktowaniu rdzenia do 850MHz otrzymujemy 2.72TFlops dla pojedynczej precyzji i 544GFlops dla podwójnej.

Niestety ciągle nie wiadomo jakie parametry będzie miał zapowiadany układ GF100 od NVidii. Brakuje informacji o częstotliwości rdzeni i dokładniejszych informacji o jednostkach SFU. Można jednak spodziewać się wyraźnie wyższej wydajności od Radeona 5870 w podwójnej precyzji i wyraźnie niższej wydajności w pojedynczej precyzji.

Na koniec warto przypomnieć że teoretyczna maksymalna wydajność to nie wszystko. Układy CPU mimo iż teoretycznie wielokrotnie wolniejsze od GPU w wielu zastosowaniach są niezastępowalne. Podobnie Radeon 4870 teoretycznie szybszy od GeForca GTX 280 przegrywa z nim w prawie każdej grze. W kolejnych postach postaram się opisać różnice w architekturze, które decydują o wydajności w realnych zastosowaniach.

Volatile a publikacja obiektu w C++

Grudzień 5th, 2009 Marcin Borowiec

W jednym z poprzednich wpisów pisałem o problemie publikacji obiektu w programie wielowątkowym. Podałem wtedy bardzo prosty przykład kodu w pseudo asemblerze. Z kolei w  języku C++ publikacja mogła by wyglądać następująco:

bool gotowe = false;
Object *object = 0;
// ...
object = new Object(a,b,c);
gotowe = true;

Jak już wiemy abyśmy mieli gwarancje że object będzie dostępny jak tylko zmienna gotowe zmieni wartość na true, musimy upewnić się, że zapisy nie zostaną przeniesione przed inne zapisy i odczyty nie zostaną przeniesione za inne odczyty. W przypadku języków C# czy Java coś takiego możemy wymusić za pomocą deklaracji gotowe jako volatile. Niestety albo na szczęście standard C++ nic takiego nie przewiduje. W takim razie co należy zrobić aby uzyskać możliwość publikowania obiektu?

  • Jeśli nasz program będzie uruchamiany wyłącznie na procesorach x86 możemy zadeklarować gotowe jako volatile. To w zupełności wystarczy, gdyż model tej architektury jest stosunkowo mocny (Po szczegóły odsyłam do moich poprzednich wpisów).
  • Jeśli nasz program będziemy kompilować wyłącznie w Visual C++ 2005 lub nowszym, podobnie jak w poprzednich punkcie mamy gwarancje że volatile zadziała. Także dla kompilacji pod IA64. Jest to specjalna właściwość kompilatora Microsoftu.
  • W pozostałych przypadkach musimy albo opakować odwołania do gotowe w sekcję krytyczną albo użyć specjalistycznej biblioteki, z funkcją która wymuszą odpowiednie memory barrier. (Co prowadzi do oszczędności czasu procesora w stosunku do sekcji krytycznej).

Odnosząc się do słówka volatile pozostają jeszcze dwa pytania. Czy w takim razie volatile jest prawie całkiem bezużyteczne? oraz co o volatile mówi przyszły standard C++ 0x? Na te pytania postaram się odpowiedzieć, w któryś z kolejnych postów.

GPGPU – podstawowe informacje

Listopad 28th, 2009 Marcin Borowiec

Zauważyłem ostatnio, że popularność GPGPU wśród użytkowników/programistów zaczęła gwałtownie wzrastać. Niestety wielu z nich nie wie co to tak na prawdę jest, jak z tego skorzystać, co trzeba posiadać (sprzęt, oprogramowanie) i jakie ma to możliwości. Postanowiłem więc przybliżyć nieco idę i możliwości GPGPU. Nie zamierzam jednak tworzyć tutaj jakiegoś kursu (przynajmniej na razie).

GPGPU to jak sama nazwa wskazuje możliwość wykorzystania procesorów graficznych do obliczeń ogólnego przeznaczenia. Idea GPGPU zaczeła się rodzić po udostępnieniu kart graficznych zgodnych z DirectX9 (głównie DirectX9c). Operacje zmiennoprzecinkowe z 32bitową precyzją, sensowna długość kodu shadera, dynamiczne instrukcje warunkowe w kodzie, pętle mogły dać początek GPGPU. Niestety w tamtym czasie jedynym możliwym sposobem wykorzystania kart graficznych było napisanie programu z wykorzystaniem DirectX czy OpenGL. Biblioteki te oprócz bezpośredniego wyświetlania obrazu na ekranie posiadają możliwość generowania wyników do tekstury. Programista mógł więc wysłać do karty graficznej dane w postaci tablicy danych (jako tekstura czy bufor wierzchołków), wygenerować wynik i pobrać wynik (teksturę). Do pewnych specyficznych operacji coś takiego wystarczyło, kiedyś czytałem o prostych operacjach na próbkach dźwiękowych dzięki takiemu chwytowi. Niestety do wielu zastowań to zdecydowanie za mało.

Kolejnym etapem było stworzenie kart graficznych z tzw. zunifikowanymi jednostkami cieniowania. Mowa tu seriach GeForce 8xxx i RadeonHD 2xxx z czego produkty NVidi były w kontekście GPGPU bardziej zaawansowane i nadal są, ale o tym nieco później. NVidia razem z premierą serii 8 GeForce’ów udostępniła technologię CUDA. Technologia ta pozwalała w dość prosty sposób wykorzystać moce drzemiące w karcie graficznej. CUDA tworzone było przede wszystkim do obliczeń GPGPU więc nie spotkamy tutaj niepotrzebnych ograniczeń. AMD(ATI) też myślało o czymś podobnym, najpierw zaczeli od ATI CTM (Close To Metal), które dość szybko anulowali bo zrozumieli że nie tędy droga a później stworzyli ATI Stream.

Obecnie interfejsów do programowania kart graficznych jest dość dużo. Poniżej wymienie najważniejsze z nich:

  • C for CUDA czyli programowanie w zmodyfikowanym przez nvidie języku C. Rozwiązanie do tej pory najpopularniejsze, procentowy rozkład użycia tej technologii może w przyszłości nieco zmaleć za sprawą interfejsów otwartych takich jak OpenCL czy DirectCompute.
  • CUDA PTX czyli możliwość pisania programów na GPU NVidii w asemblerze, w pewnych przypadkach sensownym może być utworzenie programu w C, skompilowanie go a później ręczna optymalizacja w asemblerze. Będąc scisłym, PTX nie jest asemblerem wykonywanym bezpośrednio przez GPU. PTX przed wykonaniem musi być przetłumaczony na kod maszynowy konkretnego modelu GPU, na którym będzie uruchomiony program.
  • Brook++ składnik ATI Stream w wersjach 1.x, stosunkowo prosty sposób programowania GPU, łatwy do użycia ale zarazem o niewielkich możliwościach (w porównaniu np do C for CUDA), trudno w Brook++ wykorzystać pełnię mocy Radeonów HD5xxx. Właśnie dlatego AMD przestaje oficjalnie wspierać Brook++ w ATI Stream 2.x. Mimo iż zarzekają się że projekt dalej będzie wspierany, z tym że od teraz jako oddzielny projekt open source to można uznać że jest to w tej chwili ślepa uliczka. AMD chce się teraz skupić na bardziej uniwersalnych interfejsach (OpenCL i DirectCompute)
  • AMD CAL z asemblerem AMD IL jest odpowiednikiem PTX od Nvidii. Asembler daje największe możliwości na zaoptymalizowanie kodu jednak ze wględu na poziom trudności jest bardzo rzadko stosowany.
  • OpenCL, otwarty standard rozwijany przez Khronos Group (przez tą samą grupę, która odpowiada za OpenGL), Każdy z producentów (AMD, NVidia) dostarcza implementację tego interfejsu. Khronos Group zajmuje się tylko certyfikacją gotowych bibliotek. OpenCL posiada także funkcje, które mają umożliwić lepszą współpracę z biblioteką OpenGL: m.in edycję buforów wierzchołków OpenGL z poziomu OpenCL.
  • DirectCompute (element DirectX11), biblioteka przygotowana we współpracy producentów kart graficznych z firmą Microsoft. Charakteryzuje się bardzo dobrą współpracą z innymi elementami DirectX.

Teoretycznie jest możliwe napisanie programu pod GPU nie posiadając odpowiedniego akceleratora u siebie. C for CUDA posiada możliwość emulacji napisanego kodu na CPU, OpenCL posiada implementacje także na CPU (ja przynajmniej znam jedną od AMD), DirectCompute można uruchomić poprzez urządzenie referencyjne (które jest bardzo wolne, WARP niestety nie obsługuje DirectCompute). W rzeczywistości sytuacje, które powstają podczas wykorzystania GPU nijak nie można zasymulować na CPU, tak więc bardzo mocno polecam zaopatrzyć  się w odpowiednią kartę graficzną. Jaką? Dla mnie jedynymi sensownymi układami są najnowsza seria Radeonów HD 5xxx i kart NVidii zgodnych z ComputeCapability przynajmniej 1.1 (czyli seria GTX 2xx, 9xxx i częściowo 8xxx, bez słynnego G80) a także odpowiadające im modele układów Tesla i FireStream. Dlaczego akurat takie karty?

  • Radeon HD seria 2xxx i HD seria 3xxx wypada bardzo ubogo pod względem wydajności obliczeniowej, obsługiwanych bibliotek jak i funkcji wbudowanych w GPU. Jeśli ktoś nie daje Ci za darmo takiej karty to jej nie bierz. Brak m.in obsługi OpenCL i DirectCompute!
  • Radeon HD seria 4xxx to układy pełne paradoksów. W karty wbudowano pamięć LDS – Local Data Store. NVidia coś podobnego (Shared Memory) miała już w pierwszych układach GeForce 8! Najciekawsze jest jednak to że Brook++ z racji swojej specyfikacji nie jest w stanie wykorzystać odpowiednio LDS, OpenCL mimo iż obsługiwany przez serie 4xxx to LDS nie jest zgodny z OpenCL i nie jest w ogóle wykorzystywany. DirectCompute 4.0, który teoretycznie jest kompatybilny z LDS nie jest jeszcze dostępny bo AMD nie przygotowała odpowiednich sterowników. Oprócz LDS seria 4xxx posiada pamięć GDS – Global Data Storage, która w zasadzie też nie może być wykorzystana, ponieważ Radeon 4xxx nie potrafi odpowiednio zsynchronizować dostępu do niej. Łącznie 176KB bardzo szybkiej pamięci wbudowanej wewnątrz układu nie może być wykorzystane. Brawo AMD!
  • Radeon HD seria 5xxx, w końcu układ z  bardziej zaawansowaną pamięcią LDS (pełny odczyt i zapis przez każdy wątek z grupy), obsługą operacji atomowych, obsługą konkretnego API do programowania GPU (OpenCL i DirectCompute). Tylko dlaczego dopiero teraz?
  • Jeśli chodzi o NVidię to tutaj każdy układ zgodny z CUDA jest sensowny. Każdy z tych układów dzięki nowszym sterownikom obsłuży także OpenCL i DirectCompute włącznie z wprowadzonym kilka lat temu do sprzedaży GeForcem 8800! Ja jednak polecam zaopatrzyć się w nieco nowszy układ (coś na bazie G92 albo najlepiej GT200), który posiada obsługę instrukcji atomowych.

Wrócę jeszcze na chwilę do interfejsów programowania. Myślę że można tutaj przyjąć następujący algorytm wyboru:

  • Rozszerzanie gry w DirectX o zaawansowane efekty: DirectCompute
  • Rozszerzanie gry w OpenGL o zaawansowane efekty: OpenCL
  • Wykorzystanie GPU do obliczeń innych niż graficzne: OpenCL
  • Specjalistyczne zadania niewymagające wspierania wszystkich producentów GPU: Najnowszy układ GeForce/Quadro/Tesla, C for CUDA lub ewentualnie PTX gdy konieczna jest maksymalna optymalizacja.

Na koniec pozostała jeszcze jedna ważna sprawa. Jak bardzo obecne GPGPU jest elastyczne, jakiego typu programy można uruchamiać na kartach graficznych?

  1. Obecnie żaden dotychczasowy program pisany z myślą o CPU nie może być bezpośrednio przeniesiony na GPU bez jakiejkolwiek modyfikacji kodu. Chodzi mi tutaj o programy pisane w C++, C# i innych tego typu językach.
  2. Algorytm przenoszony na GPU musi być wielowątkowy. Chodzi tutaj o rząd kilku tysięcy wątków. Co więcej każdy z tych wątków musi przetwarzać ten sam kod więc mamy tutaj do czynienia wyłącznie ze współbieżnością na poziomie danych. Oczywiście każdy wątek ma swój kontekst i wie czy powinien przetwarzać tą czy tą gałęź kodu (w przypadku pętli i instrukcji warunkowych). To ograniczenie będzie częściowo zniesione w nowym GPU NVidii “Fermi”, który zadebiutuje w przyszłym roku. Teoretycznie każdy multiprocesor będzie mógł wykonywać inny kernel (tak popularnie nazywana jest funkcja wywołana na GPU) co oznacza kilka-kilkanaście niezależnych grup wątków wykonujących inny kod.
  3. Z poziomu GPU program nie ma dostępu do pamięci głównej komputera, nie ma dostępu do urządzeń wejścia/wyjścia komputera. Część programu działająca po stronie CPU musi wysłać dane do urządzenia, uruchomić funkcję na tym urządzeniu i pobrać wyniki z urządzenia. Jeśli funkcja na CPU uzna że musi uaktualnić dane to funkcja na GPU musi zostać przerwana, nowe dane muszą zostać dostarczone i funkcja musi być ponownie uruchomiona.
  4. GPU nie obsługuje rekurencji, każdy algorytm musi być zapisany w wersji iteracyjnej.
  5. GPU nie obsługuje zaawansowanych sposobów synchronizacji wątków, mimo wszystko operacje atomowe (choć niedostępne w niektórych GPU) i bariery na grupy wątków wystarczają do zbudowania efektywnej współpracy pomiędzy wątkami. Wątki w grupie mogą odczytywać i zapisywać wspólną pamięć dzieloną. Wszystkie wątki na urządzeniu mogą odczytywać i zapisywać wspólną pamięć globalną.
  6. GPU nie posiada zaawansowanego mechanizmu cache (chociaż wspomniany wcześniej układ “Fermi” ma taki cache posiadać). Obecnie trzeba sobie pomóc trzema elementami dającymi namiastkę cache: odczyt/zapis tabel dwuwymiarowych ze specjalnym tylko do tego dedykowanym małym cache; odczyt z pamięci stałej (pamięć globalna oznaczona jako nie zmieniana podczas pojedynczego wywołania kernela), do tego też jest specjalny cache; stworzenie ręcznie zarządzanego ogólnego cache poprzez wykorzystanie wbudowanej pamięci dzielonej dla grupy wątków.

Przykładów wykorzystania GPU można szukać np. na stronie NVidii. Ja dodam że istnieje możliwość przeniesienia sztucznej inteligencji Paper Balla na GPU, robiłem już małe testy ;)

Post wyszedł dość długi a ja nie napisałem wszystkiego czego chciałem. Mimo wszystko na razie na tym zakończę, a do tematu wrócę w którymś z kolejnych mini artykułów.

Trójkąt w DirectX 11

Październik 27th, 2009 Marcin Borowiec

Chwilowo odbiegnę od głównego tematu mojej strony czyli programowania współbieżnego. Niedawno potrzebowałem przygotować małą aplikacje z wykorzystaniem DirectX. Ponieważ nie miałem narzuconej wersji, postanowiłem sprawdzić najnowszy DirectX 11. O ile do wersji 10 w DirectX SDK znajduje się dość konkretny tutorial, o tyle do jedenastki mamy pusty projekt albo kilka konkretniejszych aplikacji. To czego ja potrzebowałem to szablon aplikacji wyświetlającej zwykły trójkąt. Postanowiłem że po prostu przerobie szybko aplikacje z lekcji 2 i 3 tutoriala do DirectX 10. Okazało się że sprawa nie jestem całkiem prosta więc stwierdziłem że podzielę się tutaj napotkanymi przeze mnie problemami i ich rozwiązaniami.

Wszystkie testy przeprowadzałem na Windows 7 x64 i DirectX SDK August 2009. DirectX 11 będzie dostępny na systemie Windows Vista za pomocą platform update. Nie sprawdzałem postępu prac nad platform update i nie jestem w stanie powiedzieć czy aktualna wersja pozwoli uruchomić aplikacje DirectX 11.

Przerabiając aplikacje wyświetlającą trójkąt z dziesiątki na jedenastke trzeba zdać sobie sprawę z kilku rzeczy:

  • DirectX 11 wprowadza nowy model programowania wielowątkowego. W tym celu interfejs ID3D11Device został rozdzielony na dwa niezależne interfejsy ID3D11Device i i ID3D11DeviceContext. Dzięki temu drugiemu możliwe jest generowanie komend renderowania grafiki z wielu wątków.
  • D3DXMath został zastąpiony przez XnaMath. Zamiast np. D3DXVECTOR3 można użyć XMFLOAT3. XnaMath dostępne jest po dołączeniu xnamath.h
  • Effects są teraz dostępne jako projekt vc++ w katalogu DX SDK\Utilities\Source\Effects11. Należy je skompilować, najlepiej od razu wszystkie 4 warianty (x86 – x64, Debug – Release) aby później dołączyć header i plik lib do programu wyświetlającego trójkąt.
  • Nie istnieje funkcja do jednoczesnej kompilacji kodu hlsl i stworzenia efektu. Teraz należy najpierw skompilować shadery a później utworzyć effecty.
  • Kompilator shaderów obsługuje tylko format fx_5_0. Aby stworzyć kod zgodny z kartami DirectX 10 należy w definicji techniki przekazać odpowiedni parametr, np vs_4_0, ps_4_0.

To by było w zasadzie wszystko co jest potrzebne do wyświetlenia trójkąta w DirectX gdyby nie pewien mały problem. Przy obecnej wersji Effects w DX SDK  nie jest możliwe poprawne uruchomienie takiej aplikacji na karcie zgodnej z co najwyżej z DX10.1 (Z tego co mi wiadomo na Radeonach HD5xxx wszystko chodzi bardzo ładnie ;) ) Problem jest opisany tutaj. Podane są tam dwa przykładowe rozwiązania. W skrócie podczas wywołania D3DX11CreateEffectFromMemory wywoływane są funkcje Create…Shader interfejsu ID3D11Device. Jednym z parametrów tych funkcji jest ID3D11ClassLinkage *pClassLinkage. Parametr ten w zależność od levelu: D3D_FEATURE_LEVEL_11_0 lub inny powinien zawierać poprawny wskaźnik lub być wartością NULL. Osobiście aby u siebie uzyskać działający profil D3D_FEATURE_LEVEL_10_0 i D3D_FEATURE_LEVEL_10_1 zamieniłem linie 1272 i 1273 w pliku EffectNonRuntime.cpp w projekcie Effects11 z:

else if( FAILED( (m_pDevice->*(pShader->pVT->pCreateShader))((UINT *) pShader->pReflectionData->pBytecode, pShader->pReflectionData->BytecodeLength,m_pClassLinkage, &pShader->pD3DObject) ) )
                pShader->IsValid = FALSE;

na

else if( FAILED( (m_pDevice->*(pShader->pVT->pCreateShader))((UINT *) pShader->pReflectionData->pBytecode, pShader->pReflectionData->BytecodeLength, 0, &pShader->pD3DObject) ) )
               pShader->IsValid = FALSE;

Należy pamiętać że w ten sposób wyłączam level 11_0. Dla mnie to w tej chwili nie jest problemem ponieważ testuje ten program tylko u siebie na sprzęcie zgodnym z DX10. Jeśli jednak potrzebujemy mieć działające wszystkie profile, możemy np. dodać parametr do tej funkcji i przekazać jawnie na którym levelu operujemy i wtedy dodać ifa wywołującego CreateShader z classlinkage albo bez lub np próbować tworzyć shader najpierw z classlinkage a w razie nie powodzenia bez.

Na koniec załączam zip z projektem stworzonym  w Visual Studio 2010 beta2 wyświetlającym piękny żółty trójkąt. Program działa u mnie poprawnie na karcie AMD (ATI) zgodnej z DX10.1 i NVIDII zgodnej z DX10. W przypadku próby kompilacji załączonego kodu w poprzednich wersjach VS wystarczy stworzyć projekt win32 C++, zaimportować pliki .h i .cpp, dodać w pliku dx11.h po includach

#define nullptr 0

(nullptr to taki ładny null z VC++ 10), upewnić się że vs ma dostęp do odpowiednich headerów i libów i dodać linkerowi liby: d3d11.lib d3dx11d.lib D3DX11EffectsD.lib D3DCompiler.lib w przypadku trybu Debug i d3d11.lib d3dx11.lib D3DX11Effects.lib D3DCompiler.lib w przypadku trybu Release.

DX11Triangle (166)

d3d11.lib
d3dx11d.lib
D3DX11EffectsD.lib
D3DCompiler.lib

Memory ordering cd.

Wrzesień 21st, 2009 Marcin Borowiec

Miesiąc temu poruszyłem problem “przestawiania operacji odczytu i zapisu” w procesorach z rodziny x86. Dzisiaj postaram się rozszerzyć ten temat na inne architektury. W tym wpisie będę odnosił się przede wszystkim do dokumentu Memory Barriers: a Hardware View for Software Hackers. Najbardziej istotny jest tutaj rozdział 7 i tabela 5 na stronie 16. Znajduje się tam porównanie różnych architektur pod kątem możliwych zamian kolejności operacji odczytu i zapisu. Nie zamierzam tutaj tłumaczyć całego rozdziału, skupię się tylko na kilku ważnych faktach:

  • Istnieją architektury ze zdecydowanie luźniejszym modelem pamięci niż ten w procesorach x86.
  • Niektóre architektury pozwalają na wybranie jednego z kilku możliwych modeli pamięci przez system operacyjny.
  • Niespójność widoku pamięci różnych wątków może wywodzić się nie tylko z istnienia write buffera ale także np z luźniejszego modelu synchronizacji pamięci cache.
  • Wykonanie instrukcji atomowej nie implikuje uzyskania “total order”
  • Do wymuszania zachowania kolejności określonych operacji służą instrukcje typu “memory barrier”. Architektury o luźniejszym modelu oferują przeważnie kilka różnych instrukcji tego typu do uzyskania różnego poziomu spójności widoków pamięci.

Ten kto obejrzał film z poprzedniego wpisu wie, że model pamięci x86 pozwala na bezpieczne publikowanie obiektu. Przez publikację obiektu należy rozumieć sytuację:

mov a, r1;
mov gotowe, 1;

W powyższych przykładach agotowe to zmienne w pamięci, r1 to rejestr procesora. Zakładamy, że obiekt gotowe przed rozpoczęciem wykonywania tego kodu zawierał wartość 0. Zakładamy też, że przypisanie wartości 1 do obiektu gotowe oznacza, że obiekt a zawiera już nową poprawną wartość i ta wartość może być odczytana przez inny wątek. W takim wypadku inny wątek programu może co jakiś czas odczytywać wartość gotowe aż odczyta wartość 1. Wtedy wie że obiekt a zawiera poprawną wartość i może ją odczytać. Niestety takie rozwiązanie nie zadziała np. na procesorach z rodziny Power PC. Dla nich proces publikacji powinien wyglądać tak:

mov a, r1;
lwsync; //memory barrier
mov gotowe, 1;

W następnym poście opiszę jak informacje zawarte w tym wpisie odnoszą się do publikowania obiektów w C++.

Memory ordering w x86

Sierpień 24th, 2009 Marcin Borowiec

We wcześniejszych postach o widoku pamięci wątku i volatile wspomniałem o tym, że istnieje coś takiego jak write buffer. Jego zadaniem jest optymalizować zapis danych obliczonych przez procesor. Bez niego wydajność naszych maszyn byłaby dużo niższa. Niestety jego obecność powoduje zerwanie “sequential consistency”. Przykładowo sekwencja instrukcji:

a = r1; //zapis do pamięci

r2 = b; //odczyt z pamięci

(gdzie a i b to zmienne zlokalizowane w pamięci a r1, r2 to rejestry procesory) może być widziana na zewnątrz jako:

r2 = b;

a = r1;

Zapis został przeniesiony za instrukcję odczytu, gdyż procesor optymalizuje wykonanie programu poprzez użycie write buffer. Takie zamiany instrukcji nazywamy “memory ordering”. Optymalizując wielowątkowe programy na niskim poziomie powinniśmy wiedzieć jakie zmiany w kolejności odczytu i zapisu są możliwe na danym procesorze. Musimy także wiedzieć jak wymusić wykonanie instrukcji w określonym porządku tam gdzie jest to wymagane.

Poniżej znajduje się bardzo ciekawy wykład przedstawiony przez pracownika Intela. Tematem jest model pamięci architektury IA-32 uściślony przez Intela w 2008 roku.

Aby zrozumieć model pamięci w x86 trzeba zapamiętać i zrozumieć 8 następujących reguł:

  1. Odczyty nie są przenoszone przed lub za inne odczyty.
  2. Zapisy nie są przenoszone przed lub za inne zapisy.
  3. Zapisy nie są przenoszone przed występuje przed nimi odczyty.
  4. Odczyty mogą być przeniesione przed wcześniejsze zapisy. Nie dotyczy przypadku gdy odczyt i zapis do tyczy tej samej lokacji.
  5. Jeśli w wieloprocesorowym systemie procesor0 zapisze 1 do lokacji x i następnie jeśli procesor1 zobaczy tą wartość i zapisze 1 do lokacji y to jeśli procesor2 zobaczy 1 w lokacji y to zobaczy też 1 w lokacji x. Zakładamy że lokacje x i y zawierały w stanie początkowym wartości 0.
  6. Jeśli w wieloprocesorowym systemie  procesor0 zapisał wartość do lokacji a, procesor1 zapisał wartość do lokacji b to dowolne inne dwa procesory zobaczą te zapisy w tej samej kolejności.
  7. Jeśli w wieloprocesorowym systemie procesor0 wykonał operacje blokowaną (np. xchg czyli zapis i odczyt) na lokacji a i procesor1 wykonał operację blokowaną na lokacji b to dowolne dwa inne procesory zobaczą te operacje w tej samej kolejności.
  8. Zapisy i odczyty nie są przenoszone przed lub za instrukcje blokowane.

Polecam obejrzeć dołączony film. Te zasady są tam dobrze objaśnione. W kolejnych postach opisze jak memory ordering wygląda na innych architekturach i co z tego wszystkiego wynika.

MIT OpenCourseWare

Sierpień 10th, 2009 Marcin Borowiec

Ostatnio trafiłem na stronę bezpłatnych kursów publikowanych przez uczelnię MIT (Massachusetts Institute of Technology). Listę kursów można obejrzeć pod adresem: http://ocw.mit.edu/OcwWeb/web/courses/courses/index.htm. Gorąco polecam przejrzenie tej strony. Jest bardzo dużo różnych tematów, zarówno z dziedziny informatyki jak i innych.

Mnie szczególnie zainteresował kurs Multicore Programming Primer. Jak sama nazwa mówi jest on poświęcony programowaniu systemów wielordzeniowych. W ramach kursu dostępne są slajdy z prezentacji, video z prezentacji, przykładowe kody źródłowe programów czy quizy, które pomogą sprawdzić ile zapamiętaliśmy i zrozumieliśmy. W Multicore Programming Primer opisano m.in.:

  • Postawy związane z programowaniem systemów wielordzeniowych.
  • Procesor Cell (zawarty m.in. w konsoli PlayStation3). Procesor ten jest bardzo ciekawym przykładem, gdyż jego budowa wymusza określony sposób rozdzielania zadań na wątki czy komunikacji między wątkami. Wymaga od programisty zastanowienia się nad rzeczami, nad którym w ogóle nie trzeba zaprzątać sobie głowy pisząc program np pod Core 2 Duo czy Phenoma X4.
  • Języki programowania tworzone z myślą o procesorach wielordzeniowych: Cilk (rozszerzenie C) czy dość nietypowy StreamIt. Osobiście uważam że programowanie wielowątkowe powinno być integralną częścią języka, a nie dodatkiem w postaci zewnętrznych bibliotek (tak jak to jest w C++). Co więcej program jednowątkowy powinien być szczególnym przypadkiem programu wielowątkowego. Obecnie większość programów pisze się dokładnie odwrotnie.
  • Podstawy tworzenia kompilatorów, które automatycznie rozdzielają wykonanie kodu na wiele wątków. Ten temat szczególnie mnie zainteresował.

User Mode Scheduling w Windows 7

Maj 20th, 2009 Marcin Borowiec

Windows 7 w wersji 64bit przynosi dobre wieści dla programistów aplikacji wielowątkowych i użytkowników, którzy chcieliby jak najlepiej wykorzystać swoje wielordzeniowe maszyny. W najnowszym systemie Microsoftu wbudowano mechanizm umożliwiający zarządzanie wątkami po stronie aplikacji. Oznacza to że: 1) przełączanie pomiędzy wątkami będzie szybsze i 2) aplikacja ma wpływ na to co się dzieje podczas przełączania wątków. Dla użytkownika końcowego oznacza to lepszą skalowalność programów na wiele rdzeni i możliwość uzyskania lepszej “płynności” (szczególnie przydatne np. w grach). W tej chwili najłatwiejszym sposobem wykorzystania User Mode Scheduling jest napisanie aplikacji z wykorzystaniem Concurrency runtime dostępnym w Visual Studio 2010 beta. Warto zwrócić uwagę że UMS jest dostępny tylko w 64bitowej wersji Windows 7 i Windows 2008 R2.

Zainteresowanym polecam linki: Concurrency Runtime and Windows 7, Dave Probert: Inside Windows 7 – User Mode Scheduler (UMS). Niestety dokumentacja do UMS dostępna na MSDN jest ciągle bardzo uboga (ma zostać skończona przed premierą Windows 7). Gdy pojawi się więcej informacji, wróce do tego tematu na blogu.

Mam przy okazji pytanie dla osób preferujących systemy spod znaku pingwina. Czy istnieje podobny mechanizm dla linuksa?

GPGPU – ciekawe linki

Wrzesień 24th, 2008 Marcin Borowiec

GPGPU (General-purpose computing on graphics processing units czyli wykorzystanie kart graficznych do obliczeń ogólnego przeznaczenia) staje się coraz bardziej popularne. Przyczyniła się do tego mocno nvidia reklamując swoje “cuda”. AMD jest nieco w tyle ale dzielnie tworzy swoje Stream SDK. Ostatnio sam zabrałem się za zgłębianie tajemnic procesorów graficznych i api, które przygotowali ich producenci. Zebrałem przy tej okazji kilka ciekawych linków.

Oczywiście podstawowym źródłem informacji są strony: NVIDIA CUDA i AMD Stream. Na początek polecam AMD Stream SDK User Guide i Building a High Level Language Compiler For GPGPU ? PLDI?08 Tutorial. Dokumentacja od AMD wydaje mi się bardziej przyjazna dla początkującego niż ta od nvidii. Przede wszystkim AMD przedstawia architekturę swoich kart graficznych i na tym oparty jest opis API Stream SDK. Pozwala to lepiej zrozumieć dlaczego programy pod GPU piszę się tak a nie inaczej i jak należy programować żeby uzyskać wysoką wydajność. Z kolei nvidia serwuje nam czysty opis API technologii CUDA. Znajdziemy tam kilka nowych pojęć, które wprowadzono aby uporządkować informację o programowaniu ich GPU. Szkoda tylko że nie opisali jak to się ma do architektury GeForce. Czytając dokumentację od AMD warto wspomóc się opisami: RV770 (1) i RV770 (2) (RV770 to układ znajdujący się na kartach Radeon 4850 i Radeon 4870). Przytoczone wcześniej AMD User Guide wspomina głównie o poprzedniku (układzie RV670 znajdującym się na kartach Radeon 3870). Podstawą do programowania w technologii CUDA jest oczywiście NVIDIA Programming Guide. Jeśli po przeczytaniu zastanawiacie się co to jest do cholery ten “warp” polecam przeczytanie artykułu: NVIDIA’s 1.4 Billion Transistor GPU: GT200 Arrives as the GeForce GTX 280 & 260, szczególnie strony drugiej i czwartej.

Jeśli macie jakieś inne ciekawe linki dotyczące tematu GPGPU dopiszcie je w komentarzach :)

Kilka spraw organizacyjnych

Sierpień 8th, 2008 Marcin Borowiec

Po miesiącu prowadzenia nowej wersji strony zebrało się kilkach spraw o których chciałbym napisać. Po pierwsze na stronie ciągle zachodzą zmiany, zmieniam ustawienia, dołączam/aktualizuje pluginy do wordpresa. Może się więc okazać że coś nagle przestanie działać. Jeśli ktoś zauważy że coś nie działa będę wdzięczny za informację o tym (albo tutaj w komentarzu albo na e-maila). Po drugie zachęcam do komentowania, czy to konkretnych postów tematycznych czy samej strony. W tej chwili komentarze są moderowane(na stronie będą wyświetlone po zaakceptowaniu komentarza przeze mnie). Pozwala mi to na ręczny odsiew spamu, z którym nie poradziły sobie automaty. W przyszłości postaram się popracować nad lepszym mechanizmem antyspamowym i wyłączeniem moderacji postów. Natomiast jeśli ktoś z Was napisał komentarz i w ciągu 24h nie pojawił się na stronie proszę o informację o tym. Zapewne będzie to wina niepoprawnej konfiguracji skryptów strony. Po trzecie jeśli macie ciekawe linki, materiały o programowaniu wielowątkowym będzie mi miło jeśli mi je podeślecie. Wiedzy nigdy za wiele ;)

Obecnie zrobiłem sobie krótką przerwę od świata wirtualnego. Na nowe wiadomości zapraszam po 15 sierpnia :)