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?
Odpowiedzi:
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 danegoupdate()
połączenia. Jak już sugerowałeś, nie chcesz przydzielać swoich bytów i komponentów jako dyskretnych obiektównew
, 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.
źródło
std::vector
jest 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 implementacjestd::deque
są również „wystarczająco przylegające” (choć nie Microsoft).