Jak skorzystać z pamięci podręcznej procesora w silniku gry systemu komponentu elementu?

15

Często czytam w dokumentacji silnika gry ECS, która jest dobrą architekturą do rozsądnego używania pamięci podręcznej procesora.

Ale nie mogę zrozumieć, w jaki sposób możemy skorzystać z pamięci podręcznej procesora.

Jeśli komponenty są zapisywane w tablicy (lub puli), w ciągłej pamięci, to dobry sposób na użycie pamięci podręcznej procesora, ALE tylko wtedy, gdy czytamy komponenty sekwencyjnie.

Kiedy używamy systemów, potrzebują one listy jednostek, które są listą jednostek, które mają komponenty o określonych typach.

Ale te listy podają komponenty w sposób losowy, a nie sekwencyjny.

Jak więc zaprojektować ECS, aby zmaksymalizować trafienie w pamięci podręcznej?

EDYTOWAĆ :

Na przykład system fizyczny potrzebuje listy encji dla encji, która ma komponenty RigidBody i Transform (istnieje pula dla RigidBody i pula dla komponentów Transform).

Więc jego pętla do aktualizowania jednostek będzie wyglądać następująco:

for (Entity eid in entitiesList) {
    // Get rigid body component
    RigidBody *rigidBody = entityManager.getComponentFromEntity<RigidBody>(eid);

    // Get transform component
    Transform *transform = entityManager.getComponentFromEntity<Transform>(eid);

    // Do something with rigid body and transform component
}

Problem polega na tym, że komponent RigidBody encji1 może znajdować się w indeksie 2 swojej puli, a komponent Tranform encji1 w indeksie 0 swojej puli (ponieważ niektóre encje mogą mieć niektóre komponenty, a nie inne oraz z powodu dodawania / usuwania encji / komponenty losowo).

Więc nawet jeśli komponenty są przylegające do pamięci, są odczytywane losowo, więc będzie mieć więcej pamięci podręcznej, prawda?

Chyba że istnieje sposób, aby wstępnie pobrać kolejne składniki w pętli?

Johnmph
źródło
czy możesz nam pokazać, w jaki sposób przydzielasz poszczególne komponenty?
concept3d
Z prostym alokatorem puli i menedżerem uchwytów do obsługi odwołania do komponentów w celu zarządzania relokacją komponentów w puli (aby zachować ciągłość komponentów w pamięci).
Johnmph,
W przykładowej pętli założono, że aktualizacje komponentów są przeplatane na jednostkę. W wielu przypadkach możliwa jest zbiorcza aktualizacja komponentów według typu komponentu (np. Najpierw zaktualizuj wszystkie komponenty urządzenia sztywnego, a następnie zaktualizuj wszystkie transformacje o gotowe dane sztywnego ciała, a następnie zaktualizuj wszystkie dane renderowania o nowe transformacje ...) - może to poprawić pamięć podręczną używać dla każdej aktualizacji składnika. Myślę, że ten typ struktury sugeruje poniżej Nick Wiggill.
DMGregory
To mój przykład, który jest zły, w rzeczywistości chodzi bardziej o „aktualizację wszystkich transformacji za pomocą gotowych danych sztywnego ciała” niż o system fizyki. Ale problem pozostaje ten sam, w tych systemach (aktualizacja transformacji za pomocą sztywnego korpusu, aktualizacja renderowania za pomocą transformacji ...), będziemy musieli mieć więcej niż jeden typ komponentu w tym samym czasie.
Johnmph,
Nie jesteś pewien, czy to może mieć znaczenie? gamasutra.com/view/feature/6345/…
DMGregory

Odpowiedzi:

13

Artykuł Micka Westa wyjaśnia w pełni proces linearyzacji danych komponentów bytu. Wiele lat temu działał w serii Tony Hawk na znacznie mniej imponującym sprzęcie niż obecnie, aby znacznie poprawić wydajność. Zasadniczo używał globalnych, wstępnie przydzielonych tablic dla każdego odrębnego rodzaju danych bytu (pozycja, wynik i tak dalej) i odwołuje się do każdej tablicy w odrębnej fazie jego update()funkcji ogólnosystemowej . Możesz założyć, że dane dla każdej jednostki będą miały ten sam indeks tablicy w każdej z tych globalnych tablic, więc na przykład, jeśli odtwarzacz zostanie utworzony jako pierwszy, może mieć swoje dane [0]w każdej tablicy.

Jeszcze bardziej specyficzne dla optymalizacji pamięci podręcznej, slajdy Christer Ericsson dla C i C ++.

Aby podać nieco więcej szczegółów, powinieneś spróbować użyć ciągłych bloków pamięci (najłatwiej przypisanych jako tablice) dla każdego rodzaju danych (np. Pozycji, xy i z), aby zapewnić dobrą lokalizację odniesienia, wykorzystując każdy taki blok danych w odrębny sposób update()fazy ze względu na lokalizację czasową, tj. upewnienie się, że pamięć podręczna nie jest opróżniana przez algorytm sprzętowy LRU, zanim ponownie wykorzystasz dane, które zamierzasz ponownie wykorzystać w ramach danego update()połączenia. Jak już sugerowałeś, nie chcesz przydzielać swoich bytów i komponentów jako dyskretnych obiektów new, ponieważ dane różnych typów w każdej instancji bytu będą następnie przeplatane, zmniejszając lokalizację odniesienia.

Jeśli masz współzależności między komponentami (danymi), tak że absolutnie nie możesz sobie pozwolić na oddzielenie niektórych danych od powiązanych z nimi danych (np. Transform + Fizyka, Transform + Renderer), możesz zreplikować Dane transformacji zarówno w tablicach Fizyka, jak i Renderer , zapewniając, że wszystkie istotne dane pasują do szerokości linii pamięci podręcznej dla każdej operacji o kluczowym znaczeniu dla wydajności.

Pamiętaj również, że pamięć podręczna L2 i L3 (jeśli możesz to założyć dla platformy docelowej) robi wiele, aby złagodzić problemy, które mogą wystąpić w pamięci podręcznej L1, takie jak ograniczająca szerokość linii. Tak więc nawet w przypadku braku L1 są to siatki bezpieczeństwa, które najczęściej zapobiegają objaśnieniom do pamięci głównej, która jest o rząd wielkości wolniejsza niż objaśnienia do dowolnego poziomu pamięci podręcznej.

Uwaga dotycząca zapisywania danych Zapis nie wywołuje pamięci głównej. Domyślnie współczesne systemy mają włączone buforowanie z zapisem: zapisywanie wartości zapisuje ją tylko w pamięci podręcznej (początkowo), a nie w pamięci głównej, więc nie zostanie to utrudnione. Tylko wtedy, gdy dane są żądane z pamięci głównej (nie wydarzy się, gdy są w pamięci podręcznej) i są nieaktualne, pamięć główna zostanie zaktualizowana z pamięci podręcznej.

Inżynier
źródło
1
Tylko uwaga dla każdego, kto może być nowy w C ++: std::vectorjest w zasadzie dynamicznie skalowalną tablicą, a zatem jest także ciągły (de facto w starszych wersjach C ++ i de jure w nowszych wersjach C ++). Niektóre implementacje std::dequesą również „wystarczająco przylegające” (choć nie Microsoft).
Sean Middleditch,
2
@Johnmph Po prostu: jeśli nie masz lokalizacji odniesienia, nie masz nic. Jeśli dwa fragmenty danych są ze sobą ściśle powiązane (takie jak informacje przestrzenne i fizyczne), tj. Są przetwarzane razem, być może będziesz musiał skompresować je jako pojedynczy komponent z przeplotem. Należy jednak pamiętać, że każda inna logika (powiedzmy AI), która wykorzystuje dane przestrzenne, może wtedy ucierpieć w wyniku nieuwzględnienia danych przestrzennych obok nich . To zależy od tego, co wymaga największej wydajności (być może w twoim przypadku fizyki). Czy to ma sens?
Inżynier
1
@Johnmph tak, całkowicie zgadzam się z Nickiem, że chodzi o to, jak są przechowywane w pamięci, jeśli masz byt ze wskaźnikami do dwóch komponentów, które są daleko w pamięci, nie masz lokalizacji, muszą zmieścić się w linii pamięci podręcznej.
concept3d
2
@Johnmph: Rzeczywiście, artykuł Micka Westa zakłada minimalne współzależności. Więc: Zminimalizuj zależności; Replikacji danych wzdłuż linii pamięci podręcznej, gdzie nie można zminimalizować te obejmują np zależności ... Transform obok zarówno bryła sztywna i Render; i aby dopasować linie pamięci podręcznej, może być konieczne zmniejszenie liczby atomów danych tak bardzo, jak to możliwe ... można to częściowo osiągnąć, przechodząc od zmiennoprzecinkowego do stałego punktu (4 bajty vs 2 bajty) na wartość dziesiętną. Ale w ten czy inny sposób, bez względu na to, jak to robisz, dane muszą pasować do szerokości linii pamięci podręcznej, jak zauważono w koncepcji concept3d, aby uzyskać maksymalną wydajność.
Inżynier
2
@Johnmph. Nie. Za każdym razem, gdy zapisujesz dane transformacji, po prostu zapisujesz je w obu tablicach. To nie te napisy, o które musisz się martwić. Gdy wyślesz napis, jest tak samo dobry, jak gotowy. To brzmi , później w aktualizacji, po uruchomieniu fizyki i Renderer, że muszą mieć dostęp do wszystkich stosownych danych, natychmiast, w jednej linii cache aż bliskie i osobiste do CPU. Ponadto, jeśli naprawdę potrzebujesz tego wszystkiego razem, albo wykonujesz dalsze replikacje, albo upewniasz się, że fizyka, transformacja i renderowanie pasują do pojedynczej linii pamięci podręcznej ... 64 bajty są wspólne i to w rzeczywistości całkiem sporo danych! ...
Inżynier