poniedziałek, 28 września 2009

Złom jako stymulator postępu technologicznego

Ponieważ zmęczyło mnie ostatnio trochę hardcorowe pisanie silnika, postanowiłem zrobić sobie małą odskocznię. Wygrzebałem ze strychu laptopa, sprzęt można by powiedzieć archaiczny, bo z prockiem 266MHz, jakąś dwumegową kartą etc. i postanowiłem napisać na nim bibliotekę fizyki 2d, taką, żeby dało się na niej oprzeć płynnie działającą prostą gierkę z fizyką. Nic niezwykłego - jakiś collision response dla convexów, jakiś tam rigid body model etc. Normalnie pewnie nie zwrócił bym na to uwagi, ale z racji sprzętu jakoś mnie tknęło, gdy w jednej z funkcji musiałem zrobić coś takiego:

// pseudokod
int k = jakies obliczenia;
T* t = new T[k];
// jakies przetwarzanie
delete[] t;

Przy czym funkcja była bardzo krótka, i jak mi się wydawało, alokacja pamięci na heapie była jej wąskim gardłem (na tym sprzęcie jest to wąskie gardło co by nie mówić :P). Mogłem zastosować opcję T t[100] i liczyć, że 100 nie będzie przekroczone, ale to nie jest ani eleganckie ani profesjonalne, w ogóle to jak rosyjska ruletka. No i wtedy mnie tknęło - dlaczego T t[100] jest właściwie szybsze niż new? Dlatego, że alokacja na stosie to tylko przesunięcie wskaźnika stosu, a na heapie to wyszukanie odpowiednio dużego spójnego obszaru, ustawienie flag w jakiejś mapie pamięci (zależnie od niskopoziomowej implementacji zarządzania pamięcią), dopiero potem faktyczne przydzielenie tego obszaru i zwrócenie wskaźnika. Tak powstał pomysł napisania funkcji salloc i sfree. W pełni świadomy problemów z "popieprzeniem stosu" pomiędzy ich wywołaniami postanowiłem zrobić te funkcje jako inline, i tu pojawił się mały problem, leżący w mojej pamięci, a raczej w odwołaniu z niej do specyfikacji C++, która to nie nakazuje aby funkcja inline była faktycznie inline. Mówiąc inaczej - funkcja inline może ale nie musi pomijać całą zabawę z ramką stosu. Tak wpadłem na użycie __forceinline i z bananem na ryju odpaliłem funkcje do testów... banan szybko znikł, segfault, ale dlaczego? Rozkmina chwilę potrwała, w końcu wykonałem taki oto test:
__forceinline int stest() {
int toRet;
__asm mov toRet, esp
return toRet;
}
int t1, t2;
__asm mov t1, esp
t2 = stest;

Po wypisaniu t1 i t2 okazało się, że o ile w trybie release są one takie same, o tyle w trybie debug wartości są różne, o zgrozo! Okazało się, że programiści z Microsoftu zrobili sobie joke, nazywając słowo kluczowe __forceinline, bo tak na prawdę ono wcale nie jest force i robi z funkcji inline kiedy chce i jak chce, czyli powiedzieli "cmoknijcie nas w pompkę my i tak lepiej wiemy kiedy inline jest potrzebne". Zresztą popatrzcie co na to MSDN
Tak więc zapadła decyzja, zrobię to jak koder ANSI C - przy pomocy makro, za co zwolennicy "idealnego C++" by mnie zabili pewnie, ale to nie moja wina, że producenci kompilatorów robią sobie jaja.
#define salloc(PTR, M_SIZE) \
__asm sub esp, M_SIZE \
__asm mov PTR, esp

#define sfree(M_SIZE) \
__asm add esp, M_SIZE

Dobra, teraz pytanie - czy było warto? Do testu odpalałem funkcje w których tylko alokowałem i zwalniałem pamięć (na 1000 intów), w pętli po 10 000 000 razy (na Athlonie XP 2400+). Wyniki przeszły moje oczekiwania, może nie najśmielsze ale zawsze :P

Tryb DEBUG:
new: 23.6571
salloc: 0.596729
Tryb RELEASE:
new: 4.29237
salloc: 0.0172891

Wyniki podane są w sekundach. Zalety są kosmiczne, ale wady też są - wspomniane mieszanie na stosie na pewno nie jest zdrowe dla początkujących koderów, metoda jest raczej przeznaczona dla tych świadomych, rozumiejących co się dzieje na stosie.

czwartek, 3 września 2009

Konstruktory, konstruktory

Załóżmy, że mamy klasę, w której nie ma funkcji wirtualnych (!), po niczym nie dziedziczy (w szczególności wirtualnie), i skomponowana jest tylko z niestatycznych i niestałych obiektów podstawowych. Jest to klasyczny przypadek klas typu Vector2, Vector3, Matrix3x3, Matrix4x4, oraz wielu innych matematycznych (i nie tylko) w grach. W takich klasach bardzo często wywoływany jest konstruktor kopiujący, więc warto byłoby się zastanowić jak taki konstruktor napisać optymalnie (*). Najprostszy sposób, to kopiowanie każdej wartości osobno. Inną możliwością jest wywołanie funkcji w rodzaju memcpy, żeby cały blok danych przekopiować na raz (stąd założenie o funkcjach wirtualnych etc., żeby nie nadpisać vtable pointera), co z kolei powoduje nakład na całą zabawę z ramką stosu. Teoretycznie można to jeszcze zoptymalizować przez modyfikator inline dla konstruktora. Ostatnie podejście, które sprawdziłem to... nie pisanie konstruktora ;P
Do testu przygotowałem 4 klasy - A, B, C i D, wykorzystujące wspomniane 4 podejścia. Konstruktory kopiujące dla A, B i C wyglądają następująco (dla testu drugiego, czyli symulacji macierzy 4x4, test pierwszy odpowiadał wektorowi trójelementowemu):
A(const A& a) {
x1 = a.x1; y1 = a.y1; z1 = a.z1; w1 = a.w1;
x2 = a.x2; y2 = a.y2; z2 = a.z2; w2 = a.w2;
x3 = a.x3; y3 = a.y3; z3 = a.z3; w3 = a.w3;
x4 = a.x4; y4 = a.y4; z4 = a.z4; w4 = a.w4;
}

B(const B& b) {
memcpy(this, &b, sizeof(B));
}

inline C(const C& c) {
memcpy(this, &c, sizeof(C));
}
Następnie dla każdej z 4 klas przeprowadziłem identyczny test ( testCycles = 100000000):
for(int i = 0; i < testCycles; i++) {
A acopy = A(a);
}
Jak wspomniałem, test 1 odpowiadał Vector3, test 2 - Matrix4x4.

Można się było domyślać, że narzut wywołania funkcji przewyższy zysk przy małej liczbie elementów do skopiowania. Natomiast wynik dla klasy D, z wygenerowanym automatycznie konstruktorem kopiującym jest dosyć zastanawiający, szczególnie dla przypadku 1 (aż z wrażenia dla pewności sprawdziłem, czy kompilator w ogóle kopiował obiekty :P). Więc moja odpowiedź na pytanie (*) - jak optymalnie napisać taki konstruktor brzmi: nie pisać wcale! Kompilator sobie poradzi.

Przy okazji sprawdziłem jeszcze jedną rzecz. Wielu programistów twierdzi, że lista inicjatorów konstruktora to tylko zabieg stylistyczny i nie różni się od przypisania wartości do obiektu wewnątrz konstruktora. Oczywiście mylnie, bo stosując drugie podejście wywołujemy niejawnie konstruktor obiektu tymczasowego, potem operator przypisania, a potem jeszcze destruktor. Prosty test:
class X {
std::string text;
public:
X() {
text = "text";
}
};

class Y {
std::string text;
public:
Y() : text("text") {}
};
W pętli wywoływałem tym razem zwykły konstruktor, wynik potwierdza to, co napisałem:


wtorek, 1 września 2009

Mazanie po murach i inne zabawy

Ostatnio sporo czasu spędzam na oglądaniu produkcji niezależnych twórców, w poszukiwaniu inspiracji / pomysłu / weny - niepotrzebne skreślić. W moje ręce wpadła gierka, która wygrała w kategorii "Best Student Game" na Independent Games Festival 2009, pod złowieszczym tytułem "Tag: The Power of Paint". Po oglądnięciu screenów stwierdziłem, że to jakaś lipa - kiepsko (wręcz obciachowo) wyglądający cel shade'owy FPS, w dodatku nie widać przeciwników. No ale przecież zajął 1 miejsce w jakiejś kategorii, więc nie może być tak źle - ściągnąłem i... zakochałem się, genialna gra. Jest to bardzo nietypowe połączenie FPSa i gry logicznej - gatunków biegunowych. Do dyspozycji mamy 3 bronie - z zieloną, czerwoną i niebieską farbą oraz czwarte psikadełko - do zamazywania, coś jak gumka do ścierania. Każda farba ma pewne właściwości, gdy wejdziemy na pomalowaną nią podłogę... ścianę lub sufit. Zielona - powoduje, że nasza postać skacze, czerwona - że biegnie, niebieska - że przykleja się do ścian (dlatego można łazić po sufitach). Ogólnie chodzi o to, żeby tak bazgrać po planszy, żeby dostać się do punktu docelowego. Momentami trzeba sporo się pogłowić, popróbować różnych rozwiązań. Dodatkowo element zręcznościowy powoduje, że choć wiemy jak przeskoczyć z dachu na dach, to musimy do tego podchodzić kilka razy. No i jeszcze to łażenie po ścianach - kto grał w AVP już to przechodził, ale tutaj jest jakoś bardziej zwariowanie, momentami naprawdę mocno gubi się orientację przestrzenną. Warto zagrać. A jeszcze co ciekawe, w assetach znajdują się nieskompilowane shadery i oskryptowanie całej gry w XMLu, więc można ją sobie troszkę pozmieniać bez grzebania w binarkach.

Przy okazji wspomnę jeszcze o 2 gierkach z tego samego festiwalu. Pierwsza to rewelacyjny pomysł na gameplay - Snapshot Możemy robić screeny różnych przedmiotów (np. słonia, drzwi), potem umieszczać te screeny w jakimś miejscu, przenosząc w ten sposób owe przedmioty - np. stawiamy screena ze słoniem przy jakimś pudle, dzięki czemu słoń je przepchnie, albo drzwi na jakiejś ścianie, dzięki czemu będziemy mogli przez nią przejść. Dodatkowo klimatem przypomina troszkę czasy ośmiobitowców. Druga - FEIST spodobała mi się po prostu pod względem artystycznym, niesamowity klimat, zazdroszczę umiejętności grafikowi i designerowi (takie przeciwieństwo wspomnianego wyżej Tag :D).

A, jeszcze może wypada wspomnieć - samo IGF jest eventem odbywającym się w trakcie GDC. Wspomniana edycja odbyła się już dość dawno, w marcu. Mając fajny projekt na pewno warto się zgłosić, bo nagrody pieniężne są całkiem pokaźne, no i ten prestiż :P