Archive for the ‘Programming’ Category.

Niskopoziomowa zabawa

Zabawa w kodzie na niskim poziomie może być bardzo fajna, ja porównuję ją do rozwiązywania łamigłówek (myślę, że są osoby, które podzielają moje zdanie :)). Czasem ta zabawa sprowadza się zbadania, czy zmiana jednej funkcji assemblerowej powoduje przyspieszenie kodu o ten jeden cykl, chociaż jest to już skrajność. Przykładem tego może być funkcja obliczająca długość wektora 4D wykorzystująca funkcje wewnętrzne kompilatora – Intrinsics:

float Length()
{
   float f;
   __m128 temp = _mm_mul_ps(m_vector, m_vector);
   __m128 temp2 = _mm_add_ps(_mm_movehl_ps(temp, temp), temp);
   temp = _mm_shuffle_ps(temp2, temp2, _MM_SHUFFLE(0,0,0,1));
   _mm_store_ss(&f, _mm_sqrt_ss(_mm_add_ss(temp2, temp)));
   return f;
}

Powyższy kod jest najszybszą wersją jaką udało mi się uzyskać. Polega on na obliczeniu kwadratów wszystkich składowych, dodaniu ich do siebie oraz zpierwiastkowanie. Najbardziej pracochłonną częścią jest tutaj wbrew pozorom dodawanie, gdyż kwadrat składowych można załatwić jednym mnożeniem wektorowym. Dodawanie jednak rozbija się na dwie operacje dodawania oraz dwie operacje przemieszania, ponieważ SSE nie oferuje funkcji, która dodawałaby wszystkie elementy pojedynczego rejestru. Do wykonywania operacji przemieszania najczęściej stosuje się funkcje _mm_shuffle(), ponieważ pozwala ona dowolnie rozmieścić elementy w rejestrze. Rozszerzenie SSE umożliwia jednak wykonanie przemieszań za pomocą innych funkcji (_mm_unpackhi_ps(), _mm_unpacklo_ps(), _mm_movehl_ps(), _mm_movelh_ps()), które mieszają elementy tylko w określony sposób. W powyższym kodzie zastosowałem właśnie _mm_unpackhi(), ponieważ funkcja zapisuje elementy x i y na pozycji elementow z i w.

Inną sprawą jest, w jaki sposób kompilator radzi sobie z takim kodem. Otóż posiada on specjalnie zaprogramowane optymalizacje dla tego typu funkcji oraz typu __m128. Kompilator widząc te funkcje zamienia je na wywołania odpowiednich funkcji w kodzie assemblera, przy czym sam zarządza wykorzystaniem rejestrów xmm. Nie trzeba się zatem martwić o ilość zmiennych typu __m128, ponieważ nie wpływa to bezpośrednio na rejestry.

Fenomen SSE

Na warsztacie pojawił się ostatnio bardzo ciekawy temat dotyczący wykorzystania instrukcji procesora przetwarzających dane potokowo. Jak wiadomo każdemu programiście, każdy procesor wspiera różny zestaw funkcji (ponieważ są one dołączane do poprzednich). O to lista tych rozszerzeń:

  • MMX (operacje na liczbach całkowitych)
  • SSE (operacje zmiennoprzecinkowe pojedynczej precyzji)
  • SSE2 (operacje zmiennoprzecinkowe podwójnej precyzji)
  • SSE3 (konwersje i dodawanie w poziomie)
  • SSSE3 (dodatkowe działania na liczbach całkowitych)
  • SSE4 (dodatkowe instrukcje wektorowe, w tym upragniony iloczyn skalarny dwóch wektorów)

O ile rozszerzenie MMX korzysta z rejestrów koprocesora (co czyni go niewydajnym, ponieważ przełączanie między MMX a koprocesorem trwa trochę czasu), o tyle pozostałe rozszerzenia wprowadzają dodatkowe rejestry xmm (8 dla procesorów 32-bitowych oraz 16 dla procesorów 64-bitowych) po 128 bitów każdy. Rejestry te są traktowane tak jak jest to wymagane do danej funkcji, zatem dla liczb całkowitych jest całe 128-bitowe słowo lub można dzielić na pół, aż do uzyskania 16 osobnych bajtów. W przypadku liczb zmiennoprzecinkowych są to 4 liczby typu float lub 2 liczby typu double.

Dostępne funkcje pozwalają wykonywać obliczenia zarówno skalarnie jak i wektorowo. Oczywiście najlepiej jest gdy przeprowadzane operacje są głównie wektorowe, ponieważ pozwala to zaoszczędzić mnóstwo cennych cykli procesora. Największy problem to oczywiście pisanie operacji przy użyciu tych funkcji, ponieważ wymagana jest podstawowa znajomość Assemblera, chociaż dzięki tzw. funkcjom intrinsics wcale nie trzeba robić w kodzie wstawek asemblerowych, gdyż funkcje robią to zamiast programisty.

Przyznam szczerze, mnie również  bardzo interesują te funkcje, dlatego postanowiłem z ich pomocą napisać własną implementacje wektorów i macierzy (również dlatego, że te w D3DX, nie mają tego wsparcia). Jest to dość żmudna robota, ponieważ trzeba jakoś zagwarantować to, żeby moje wektory ruszyły na starszych komputerach (a nuż trzeba będzie), ale mimo wszystko uważam, że warta zachodu. Mogę już powiedzieć, że funkcja obliczająca długość wektora, dzięki instrukcją z rozszerzenia SSE, jest szybsza od wersji z D3DX.

NLib

Postanowiłem udostępnić moją “bibliotekę”, którą posługuję się od jakiegoś czasu. Nie jest ona jakoś super wypasiona, ale zawiera wszystkie elementy, które są mi aktualnie potrzebne, czyli:

  • Moduł podstawowy – konwersje, typedefy, jakieś funkcje liczące w czasie kompilacji
  • Profile
  • Logger
  • FStream – strumień dla plików w oparciu o funkcje WinApi
  • StructReader – funkcja do obsługi bardzo prostych plików konfiguracyjnych
  • Timer – obsługa zegara, licznik FPS w oparciu o QPC lub GetTickCount()
  • Window – prosta klasa do obsługi okna

Update’y pojawiają się wraz z pomysłami i zapotrzebowaniem na nowe wynalazki, więc nie wiem kiedy nowa wersja.

Pobierz

Assert

Ostatnio dowiedziałem się o bardzo fajnej funkcji jaką udostępnia biblioteka standardowa, czyli o funkcji Assert. Funkcja ta ma tę zaletę, że warunek, który jej przekazujemy jest sprawdzany tylko, gdy jest umieścimy definicje NDEBUG. Wtedy, gdy warunek zwróci fałsz, zostaje wyświetlone okienko, gdzie i kiedy miało zdarzenie. W przeciwnym wypadku, gdy nie zdefiniujemy NDEBUG, sprawdzanie warunku po prostu znika, dzięki czemu osiągamy większą wydajność.

Oczywiście standardowa makrodefinicja nie jest idealna, ponieważ wyświetla różne rodzaje okienka w zależności od tego, czy używamy main czy WinMain. Ale napisanie swojego odpowiednika tej funkcji nie nastręcza problemów, wystarczy się przyjrzeć makrodefinicji zawartej w pliku assert.h. Polecam również artykuł w książce Perełki Programowania Gier część pierwsza, rozdział 1.12. Są tam świetne porady na ten temat.

StructReader

Wpadłem na pomysł prostego serializer’a, który by potrafił czytać dane z pliku podzielonego na sekcje i zapisywać je do prostej struktury – agregata. Miał być to szablon funkcji, który pobierałby jako jeden z argumentów wskaźnik do tej struktury, następnie wyliczał miejsce zmiennej w strukturze, na podstawie tablicy z typami (enum) i w końcu zapisywał przeczytane dane odpowiednio, sformatowane przez bibliotekę standardową. Niestety jak się okazało (czego do tej pory nie wiedziałem) miedzy składnikami klasy, struktury są przerwy, więc nie można skorzystać z wskaźnika, którego chciałem przesuwać o rozmiar przeczytanego typu, gdyż spowoduje to błędy zapisu.
Na szczęście rozwiązanie tego problemu okazało się bardzo proste i jest o wiele bardziej uniwersalne, otóż zamiast wskaźnika na obiekt struktury, przekazuję funkcji (ponieważ szablon jest już niepotrzebny) tablicę wskaźników do zmiennych i jej rozmiar. Dzięki niej wiem gdzie w pamięci znajduje się dana zmienna. Rozwiązanie to ma też inna zaletę, ponieważ do funkcji mogę przekazać nie tylko obiekty struktury, ale również dowolne obiekty nienależące do struktury.

Oto przykładowy plik cfg:

1
2
3
4
5
6
7
8
Sekcja1{
   // Komentarz
}
// Komentarz 2
Sekcja2
{
   zmienna1 = 3.14;     // Komentarz np. opisuje jaki to typ, tu float
}

Jak widać schemat jest prosty:

  1. Sekcje oznaczamy nazwą i klamerkami
  2. Komentarze tylko po //, są ważne do końca linii
  3. Nazwy zmiennych nie są brane pod uwagę, liczy się znak ‘=’ i ‘;’, pierwszy rozpoczyna czytanie wartości, drugi kończy.

PS. Schemat pliku zmyślony z Regedit’a :).

W kodzie wygląda to tak:

1
2
3
4
5
6
7
8
9
10
11
// Tworzymy tablicę z typami
int types[] = { TYPE_FLOAT };        // Przykładowo float
 
// Zmienna
float temp;
 
// Tablica ze wskaźnikami
void* pointers = { &temp };
 
// Odczyt z pliku
FillUpStruct("nazwa_pliku_jako_string", types, sizeof(types),  "Sekcja2", pointers, sizeof(pointers));

Według mnie wygląda to ładnie, i jest proste w użyciu, szczególnie przydatne przy zapisie ustawień np. urządzenia DX.