05 grudzień 2008

Więcej mocy

Współczesne gry są bardzo wymagającymi aplikacjami. Renderowanie dynamicznego, pełnego efektów, obrazu ze stałą prędkością co najmniej 30 FPS staje się coraz trudniejszym zadaniem, nawet dla obiektywnie mocnych komputerów. Dobra optymalizacja kodu jest podstawą do sprawnego i szybkiego działania programu. Mówiąc "optymalizacja" mamy zazwyczaj na myśli lepsze konstruowanie algorytmów lub wykorzystanie innych technik dążących do rozwiązania problemu. Czasem jednak, trzeba przejść "o jeden poziom niżej" - stworzyć kod, który jest bliżej określonego sprzętu. Najprościej sytuacja wygląda na konsolach. Tam developer ma z góry ustalona platformę na którą zostanie stworzona gra. Jest więc w stanie stworzyć takie funkcje, które będą działać maksymalnie szybko. Gorzej jest w przypadku PC. Jak powszechnie wiadomo: ilu graczy, tyle różnych konfiguracji. Istnieją jednak pewne ustalone standardy. W przypadku kart graficznych definiowane są one poprzez obsługę odpowiednich wersji DirectX lub OpenGL. Co prawda, różne rozwiązania sprzętowe sprawiają, że wydajność renderowania może być różna, jednak minimalne wymagania zostają ustalone. W kwestii programisty, zostaje odpowiednio wydajne wykorzystanie powyższych API. Wbrew pozorom, procesory także posiadają możliwości, które jesteśmy w stanie bezpośrednio wykorzystać. Mowa tutaj o dodatkowych rozszerzeniach multimedialnych. Są to specjalne instrukcje, dzięki którym możemy znacznie przyśpieszyć działanie naszej aplikacji:
  • MMX (Początkowo Intel, teraz także AMD)
  • 3DNow! (AMD)
  • SSE(Początkowo tylko Intel, teraz także AAMD)
MMX. Jest to zestaw dodatkowych 57 instrukcji wykonujących operacje arytmetyczne i logiczne w technologii SIMD. SIMD (Single Instruction, Multiple Data) umożliwia nam dokonanie obliczeń za pomocą jednej instrukcji na grupie danych, spakowanych do jednego rejestru. Dzięki MMX programista mógł wykonywać równoległe obliczenia na maksymalnie dwóch, 32 bitowych zmiennych. Całość posiadała jednak, z dłuższej perspektywy, dość istotną wadę: Intel nie przeznaczył żadnych dodatkowych rejestrów dla MMX. Wszystkie operacje odbywały się na ośmiu 64 bitowych aliasach rejestrów FPU, co zmuszało koprocesor do przełączania kontekstów działania - w niektórych przypadkach kod MMX był wolniejszy od tego tradycyjnego.

3DNow! Była to odpowiedź AMD na MMX. Założenia są bardzo podobne to tych zastosowanych u konkurencji, aczkolwiek nie obeszło się bez pewnych różnic. Podstawową był typ danych na których mogły być wykonywane operacje: u AMD były to liczby zmiennoprzecinkowe pojedynczej precyzji. Poza tym, dla 3DNow! przygotowano tylko 21 instrukcji, jednak w kolejnych generacjach procesorów zestaw ten powiększał się.

SSE. Technologia z pozoru wydaje sie być podobna do wyżej zaprezentowanych, jednak wprowadzenie jej wraz z procesorami Pentium III było dość istoną ewolucją. Do dyspozycji programisty oddano osiem oddzielnych, 128 bitowych rejestrów, mogących przechować do czterych wartości zmiennoprzecinkowych pojedynczej precyzji. Dzięki temu wzorst wydajności jest zuważalny, szczególnie podczas wykonywania operacji wektorowych i macierzowych. Kolejne genereacje SSE dodają nowe instrukcje, dzięki czemu czesc operacji matematycznych (np. dot product), można wykonać za pomocą jednego wywołania instrukcji w assemblerze.

C++ 50 efektywnych sposobów na udoskonalenie Twoich programów

Autor: Scott Meyers
Wydawnictwo: Helion

Jest to jedna z bardziej oryginalnych książek na temat programowania w C++, jaką miałem przyjemność czytać. Autor w sposób lekki i przystępny, tłumaczy dość zawiłe dla początkującego programisty, elementy języka. Tytułowymi "sposobami", są poszczególne rozdziały książki - dodatkowo pogrupowane w tematyczne bloki. Dowiemy się więc jak zarządzać pamięcią, poprawnie implementować konstruktory i destruktory, zapoznamy się z projektem i implementacją funkcji i klas, oraz poznamy kilka reguł projektowania obiektowego. Praktycznie rzecz biorąc, cała merytoryczna zawartość jest już przedstawiona w spisie treści - każdy rozdział to osobny "sposób". Książki nie należy traktować jako całości. Nie jest to kurs C++, który powoli wprowadza w tajniki języka. Każdy rozdział traktuje o osobnym zagadnieniu, choć w większości autor zaznacza, w którym miejscu książki poruszony został podobny temat. Poczucie humoru, oraz lekkość z jaką Scott Meyers tłumaczy kolejne zagadnienia, sprawia, że tą dość krótką książkę (244 strony) możemy przeczytać w jeden dzień. Wiedza jednak, warta będzie wydanych pieniędzy.

04 grudzień 2008

Zarządzanie pamięcią

Szybkość przydziału pamięci na ładowane zasoby jest, wbrew pozorom, dość istotną kwestią działania niektórych aplikacji. Co prawda w grach, użytkownik zazwyczaj cierpliwie czeka, aż całość zostanie załadowana, jednak wydłużanie tego czasu może być mało pożądanym efektem. Warto zaznaczyć, że posiadacze konsol mogą być w nieco lepszej sytuacji. W przypadku tych urządzeń, jeśli średni czas ładowania określonej lokacji lub etapu, jest dłuższy, gra musi czymś zabawić czekającego gracza :). Aplikacjami codziennego użytku, w których wydajne zarządzanie i alokowanie pamięci jest ważne, są przeglądarki internetowe. Szybkość renderowania stron, przechodzenia pomiędzy zakładkami oraz zamykania programu, jest dość mocno uzależniona od kodu zarządzającego pamięcią.

Jest wiele sposobów dzięki którym możemy skrócić czas wyświetlania się komunikatu "Loading..." ;). Podstawową sprawą jest sposób zapisu danych na dysku. Powszechnie wiadomo, operacje na wielu małych plikach wykonują się dłużej, niż na jednym, im równoznacznym. Możemy więc stworzyć własny wirtualny system plików (VFS). Na czym on polega? Najczęściej spotykana implementacja takiego rozwiązania, to spakowane archiwa (rar, zip, 7z...). Jeden plik przechowuje w sobie dane wielu plików, które dodatkowo są spakowane. Najbardziej klasycznym przykładem, gry która korzysta z tego rozwiązania jest Quake 3. Zasoby gry były spakowane w oddzielne pliki *.pk, w których to dodatkowo zastosowano kompresję.

Podobnie sprawa ma się z pamięcią. Przydział kilkuset niewielkich porcji pamięci, jest znacznie mniej wydajny niż jednorazowe wywołanie funkcji przydzielającej pamięć dla większego bloku danych.
Dzięki operatorowi placement new, możliwe jest przydzielanie pamięci pod wskazany adres. Co nam to daje? Przydzielamy określony blok pamięci (ustalony z góry, lub obliczony) i posługując się wskaźnikiem na ową pamięć wywołujemy wyżej wspomniany placement new, który przydzieli zasoby pod określonym mu adresem. Następnie (po wcześniejszym rzutowaniu na char*), zwiększamy ten wskaźnik o rozmiar zablokowanego obiektu (operator sizeof()), by podczas kolejnego przydziału nie nadpisać istniejących danych. Dzięki temu dodawanie kolejnych obiektów będzie odbywało się znacznie mniejszym kosztem. Pamiętać należy tylko o tym, że wcześniej zaalokowana pamięć nie jest elastyczna - kiedyś się wyczerpie. Najlepiej, jeśli wcześniej posiadamy informacjie o ostatecznym rozmiarze ładowanych danych i dla niego przydzielimy pamięć.

Przedstawione sposoby, mimo, że bardzo proste, potrafią czasem dość znacząco przyśpieszyć działanie naszych superwydajnych aplikacji ;).

W następnych notkach postaram napisać coś o optymalizacji obliczeń matematycznych ;)