Wydajność RAII w gamedevie.

0

Z wiki. https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization

At the 2018 Gamelab conference, Jonathan Blow explained how use of RAII can cause memory fragmentation which in turn can cause cache misses and a 100 times or worse hit on performance.

Czy te spadek wydajności, o którym mowa jest chwilowy? Coś jak GC w javie? Obecnie staram się używać shared_ptr choć zdaję sobie sprawę, że to nie jest super wydajne rozwiązanie. Trzeba być naprawdę kozakiem, żeby sobie bez RAII zarządzać alokowaniem pamięci. Nawet nie potrafię sobie tego wyobrazić w game loopie.

1

Największą wpływ na wydajność ma samo tworzenie i likwidowanie obiektów, zwłaszcza przydzielanie i zwalnianie zasobów związanych z obiektem. Moim zdaniem, spadek wydajności jest właśnie chwilowy, w chwili uzyskiwania dostępu do zasobu. Jeżeli na przykład z obiektem związany jest plik, to nawet, jak nie stosuje się RAII, to i tak prędzej czy później jest potrzebne otwarcie pliku.

Metoda tworzenia i likwidowania obiektu (tradycyjne new/delete lub shared_ptr) teoretycznie też ma wpływ na wydajność, ale w większości przypadków ten wpływ jest pomijanie mały, a użycie shared_ptr lub unique_ptr znacznie zmniejsza ryzyko najczęstszych błędów związanych z zarządzaniem pamięcią, które zarazem bywają trudne do stwierdzenia.

Dużo zależy od tego, co tworzysz, ale moim zdaniem, o ile jest taka możliwość, najlepiej będzie ograniczyć tworzenie i likwidowanie obiektów do minimum, zamiast tego np. użyć wielokrotnie tego samego obiektu.

Na przykład, jeżeli co chwilę tworzysz i likwidujesz obiekt i nie możesz kontrolować liczby obiektów danej klasy, to można zrobić pulę obiektów.

1

https://en.wikipedia.org/wiki/AoS_and_SoA . RAII zachęca do AoS. Samo w sobie RAII nie jest złe czy wolne: po prostu dzięki wygodzie możesz wybrać podejście AoS, gdzie bez tej wygody być może zdecydowałbyś się na SoA. Tyle

Trzeba być naprawdę kozakiem, żeby sobie bez RAII zarządzać alokowaniem pamięci. Nawet nie potrafię sobie tego wyobrazić w game loopie.

Zamiast alokacji wrzucasz wszystko do vectora. Proste jak drut

3

At the 2018 Gamelab conference, Jonathan Blow explained how use of RAII can cause memory fragmentation which in turn can cause cache misses and a 100 times or worse hit on performance

RAII jako technika samoa w sobie nie powoduje fragmentacji pamięci. Sytuacja robi się jaśniejsza gdy ma się nieco więcej kontekstu w postaci szerszych i nieco luźniejszych wypowiedzi Jonathana Blowa. To co konkretnie ma na myśli to to częste wołanie globalnego new/delete albo malloc/free co może faktycznie powodować fragmentacje, a RAII dodatkowo może zachęcać do takiej złej praktyki dając dodatkowe bezpieczeństwo.

To o czym Jonathan Blow nie wspomina to fakt, że RAII nie jest równoznaczne z wołaniem new/delete w zakresie, a równie dobrze może operować na alokatorach czy arenie pamięci jaką sobie przygotowałeś. Także wytykanie palcem RAII to bardziej powieszchowny skrót myślowy, gdzie faktycznym problemem jest ciągła alokacja i dealokacja na stercie dla małych pierdół.

(edit)

Trzeba być naprawdę kozakiem, żeby sobie bez RAII zarządzać alokowaniem pamięci

Szczerze napisawszy, to jak sobie dobrze poukładasz arenę, to RAII naturalnie przestaje byc potrzebne, szczególnie przy robieniu gierek, gdzie jesteś w stanie sporo przewidzieć bo duża część danych jest pod Twoją kontrolą. Tym nie mniej, i tak co jakiś czas możesz potrzebować scratch memory np. do jakś danych do debugowania i RAII cały czas może być użyteczne.

A jeśli codzi o Jonathana Blowa to zapewniam Cię, że też RAII używał/używa np. do operowania muteksami.

1

Trzeba być naprawdę kozakiem, żeby sobie bez RAII zarządzać alokowaniem pamięci. Nawet nie potrafię sobie tego wyobrazić w game loopie.

Dodam jeszcze, że ważna jest optymalizacja kluczowego kodu. Przykładowo jak gra w kółko oblicza skomplikowane AI to warto, żeby wszystkie obiekty odpowiedzialne za te obliczenia (stan mapy, wszyscy aktorzy) byli zaprojektowani w taki sposób, żeby używać tej samej pamięci zamiast co chwile robić nowe obiekty. Layout (taki jak SoA) też się przydaje. Patterny takie jak ECS https://en.wikipedia.org/wiki/Entity_component_system są znamy sposobem na projektowanie gry w taki sposób, żeby było wydajnie i czytelnie

Oczywiście jak masz mało kluczowy kod np. coś co się wykonuje raz na parę sekund np. przyszła wiadomość z chatu albo gracz coś craftuje to nie musisz się spinać, bo kluczem jest optymalizacja tego co się dzieje co klatkę, bo jest ich po prostu dużo

1

C++ jest niskopoziomowy to możesz w takim vektorze objekty przechowywać lub wskaźniki na obiekty, co wtedy w drugim przypadku każdy obiekt będzie gdzieś indziej bo tak allokator mógł przydzielić, a w pierwszym przypadku będzie jeden blok pamięci continoius gdzie będą wszystkie obiekty.
Są plusy i minus, gorzej jest dodawać nowe elementy, chyba że z zapasem się zarezerwuje, usuwanie też jest utrudnione, a przy pointerach jest łatwiej bo jak usuniesz to łatwiej przesunąć pamięć.

Cache jest też set associative w cpu i adres zależy od setu w jakim się znajdzie i może być do przykładowo 8 elementów cache w jednym secie więc dopiero jak się wypchnie poprzednie dane z cache to następne odwołanie pod ten sam adres będzie wolniejsze, stosuje to się przy atakachach side channel gdzie celowo nadpisujesz cały cache i potem porównując czas dostępu możesz przewidzieć do czego uzyskał inny proces dostęp, gdyż on ponownie sprawi, że dany adres się załaduje do cache przez użycie tego.
Albo np. w starych atakach robiło się instrukcję, która powodowała crash, ale ty kontrolowałeś tablicę i index pointera, gdzie indexem była wartość kernela, gdzie procesor wykonując obliczenia do przodu i w razie błędu rollback to załadowywał strony cache, które można było badać, której tablicy był każdy element wielkości cache line i jak kernel dostał seqfault to ty badając który element tablicy ma szybki dostęp mogłeś przewidzieć jaki jest bajt w pamięci kernela.
Działało to na zasadzie branch prediction gdzie procesor miał rozgałęzienia i błędnie przewidział co ma się wykonać wywoływał ahead wyciągnięcie bajta z pamięci i użycie go do dostępu do tablicy, co skutkowało załadowaniem tego elementu, który był o wielkości cache line do cache i potem był rollback lub crash i drugi program mający dostęp do tego regionu sobie badał, ile czasu zajmuje dostęp do każdego elementu i ten, który był najkrótszy to był bajt, który znajdował się w pamięci kernela.

Przy continous memory, cache jak ładuje jeden cache line to jest optymalizacja, która ładuje automatycznie też następny obok cache line, więc jak dane leżą obok siebie to jak korzystasz z jednego fragmentu to już procesor ładuje następny obok, jak przejdziesz na następny załadowany to ładuje w tym czasie już następny.
Ogólnie procesor ma wiele funkcji przewidywania, nawet nie wszystkie są udokumentowane bo są na poziomie hardware, w apple było pełne błędów, gdzie każdy pointer jaki występował w danym cache linie, który procesor właśnie wykonywał instrukcje to z góry ładowało wszystkie adresy do pamięci i to było jednym z kluczowych elementów side channel attaku na tą platformę, ale jakoś ją potem zpatchowali.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.