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.

3 komentarze:

  1. Sprawa jest prosta - w trybie debug wszystkie "inline" są pomijane, bo inaczej debugowanie nie miałoby sensu. :)

    OdpowiedzUsuń
  2. No właśnie, ale teraz kwestia jak to zrobić? Rozwiązaniem może być define dwóch wersji - na debug funkcja alokuje na heapie, na release na stercie. Ale problem może się zdarzyć jak jakiś kruczek w kompilatorze spowoduje, że funkcja się nie rozwinie jako inline tylko poleci na ramkę ;) Wtedy jest crash :P

    OdpowiedzUsuń
  3. Jak tak się teraz głębiej zastanawiam to ogólnie jest to mało bezpieczna metoda. Aczkolwiek jest to pewna opcja na optymalizację. Życie to sztuka wyborów ;p

    OdpowiedzUsuń