Dobre pytanie. Jednak, o ile mogę powiedzieć, nie ma prostego rozwiązania tego problemu. Istnieje wiele rozwiązań (niektóre z nich wspomniałeś), ale nie widzę natychmiastowego rozwiązania srebrnej kuli.
Najpierw spójrzmy na cel. Celem nie jest umieszczenie wszystkich danych w liniowych tablicach, które są jedynie środkiem do osiągnięcia celu. Celem jest optymalizacja wydajności poprzez zminimalizowanie chybienia pamięci podręcznej. To wszystko. Jeśli korzystasz z obiektów OOP, dane Twoich jednostek zostaną otoczone danymi, których nie potrzebujesz. Jeśli twoja architektura ma rozmiar linii pamięci podręcznej 64 bajty i potrzebujesz tylko wagi (float), position (vec3) i velocity (vec3), używasz 28 bajtów, ale pozostałe 36 bajtów i tak zostanie załadowane. Jeszcze gorzej, gdy te 3 wartości nie są w pamięci obok siebie lub struktura danych pokrywa się z granicą linii pamięci podręcznej, załadujesz wiele linii pamięci podręcznej tylko na 28 bajtów faktycznie używanych danych.
To nie jest takie złe, gdy robisz to kilka razy. Nawet jeśli robisz to sto razy, prawie tego nie zauważysz. Jednak jeśli robisz to tysiące razy na sekundę, może to stanowić problem. Wejdź więc do DOD, gdzie optymalizujesz wykorzystanie pamięci podręcznej, zwykle tworząc liniowe tablice dla każdej zmiennej, w sytuacjach, w których istnieją liniowe wzorce dostępu. W twoim przypadku tablice masy, pozycji, prędkości. Po załadowaniu pozycji jednej encji ponownie ładujemy 64 bajty danych. Ale ponieważ twoje dane pozycji są obok siebie w tablicy, nie ładujesz 1 wartości pozycji, ładujesz dane dla 5 sąsiednich obiektów. Następna iteracja pętli aktualizacji będzie prawdopodobnie wymagać następnej wartości pozycji, która była już załadowana do pamięci podręcznej, i tak dalej, aż tylko do szóstej jednostki będzie musiała załadować nowe dane z pamięci głównej.
Celem DOD nie jest wykorzystanie tablic liniowych, ale maksymalizacja wykorzystania pamięci podręcznej poprzez umieszczenie danych dostępnych w (około) tym samym czasie sąsiadującym z pamięcią. Jeśli prawie zawsze uzyskujesz dostęp do 3 zmiennych w tym samym czasie, nie musisz tworzyć 3 tablic dla każdej zmiennej, możesz równie łatwo utworzyć strukturę, która zawiera tylko te 3 wartości i utworzyć tablicę tych struktur. Najlepsze rozwiązanie zawsze zależy od sposobu korzystania z danych. Jeśli twoje wzorce dostępu są liniowe, ale nie zawsze używasz wszystkich zmiennych, idź do oddzielnych macierzy liniowych. Jeśli twoje wzorce dostępu są bardziej nieregularne, ale zawsze używasz wszystkich zmiennych w tym samym czasie, umieść je w strukturze razem i utwórz tablicę tych struktur.
Twoja odpowiedź jest krótka: wszystko zależy od wykorzystania danych. Z tego powodu nie mogę bezpośrednio odpowiedzieć na twoje pytanie.Mogę dać ci kilka pomysłów na to, jak sobie poradzić z twoimi danymi, i sam możesz zdecydować, który byłby najbardziej przydatny (jeśli którykolwiek z nich jest) w twojej sytuacji, a może możesz go dostosować/zmylić.
Można zachować najbardziej dostępne dane w ciągłej tablicy. Na przykład pozycja jest często używana przez wiele różnych komponentów, więc jest to główny kandydat do ciągłej tablicy. Z drugiej strony waga jest używana tylko przez komponent grawitacyjny, więc tutaj mogą występować przerwy. Zoptymalizowano pod kątem najczęściej używanego przypadku i uzyskasz mniejszą wydajność w przypadku danych, które są rzadziej używane. Nadal nie jestem wielkim fanem tego rozwiązania z wielu powodów: jest wciąż nieefektywny, załadujesz zbyt dużo pustych danych, im niższy stosunek # określonych komponentów/# całkowitych elementów, tym gorzej. Jeśli tylko jedna na osiem jednostek ma komponenty grawitacji, a te jednostki są rozmieszczone równomiernie w macierzach, nadal otrzymuje się jedną brakującą pamięć podręczną dla każdej aktualizacji. Zakłada również, że wszystkie jednostki będą miały pozycję (lub cokolwiek innego, co jest wspólną zmienną), trudno jest dodawać i usuwać byty, jest nieelastyczne i brzydkie (w każdym razie). To może być jednak najłatwiejsze rozwiązanie.
Innym sposobem rozwiązania tego problemu jest użycie indeksów. Każda tablica dla komponentu zostanie spakowana, ale istnieją dwie dodatkowe tablice, jedna do uzyskania identyfikatora jednostki z indeksu tablicy komponentów, a druga do uzyskania indeksu tablicy komponentów z identyfikatora jednostki. Powiedzmy, że pozycja jest dzielona przez wszystkie jednostki, podczas gdy ciężar i prędkość są używane tylko przez Grawitację. Możesz teraz wykonywać iteracje na spakowanych tablicach wagi i prędkości, a aby uzyskać/ustawić odpowiednią pozycję, możesz uzyskać wartość istotności grawitacji -> entityID, przejść do komponentu Position, użyć jej entityID -> positionIndex, aby uzyskać poprawny indeks w Tablica pozycji. Zaletą jest to, że dostęp do waszej wagi i prędkości nie będzie już powodować braków w pamięci podręcznej, ale nadal otrzymacie pomyłki w pamięci podręcznej dla pozycji, jeśli stosunek składników # grawitacji/# elementów pozycji jest niski. Otrzymasz również dodatkowe 2 wyszukiwania tablicowe, ale 16-bitowy indeks unsigned int powinien wystarczyć w większości przypadków, więc te tablice będą ładnie pasować do pamięci podręcznej, co oznacza, że w większości przypadków może to nie być bardzo kosztowna operacja. Nadal profil profilu, aby być tego pewnym!
Trzecią opcją jest powielanie danych. Teraz jestem pewien, że nie będzie to warte wysiłku w przypadku twojego komponentu Gravity, myślę, że jest to bardziej interesujące w ciężkich sytuacjach obliczeniowych, ale weźmy to jako przykład. W tym przypadku komponent Gravity ma 3 upakowane tablice określające wagę, prędkość i położenie. Ma również podobną tabelę indeksu do tego, co widziałeś w drugiej opcji. Po uruchomieniu aktualizacji komponentu Gravity najpierw aktualizujesz tablicę pozycji z oryginalnej tablicy pozycji w elemencie Position, używając tabeli indeksów jak w przykładzie 2. Teraz masz 3 upakowane tablice, które możesz wykonywać obliczenia liniowo z maksymalną pamięcią podręczną wykorzystanie. Po zakończeniu skopiuj położenie z powrotem do oryginalnego komponentu Pozycja za pomocą tabeli indeksów. Teraz nie będzie to szybsze (w rzeczywistości prawdopodobnie wolniejsze) niż druga opcja, jeśli użyjesz go do czegoś takiego jak Gravity, ponieważ tylko raz czytasz i piszesz pozycję. Załóżmy jednak, że masz komponent, w którym jednostki współdziałają ze sobą, przy czym każda aktualizacja wymaga wielu odczytów i zapisów, może być szybsza. Jednak wszystko zależy od wzorców dostępu.
Ostatnią opcją, którą wymienię, jest system oparty na zmianach. Możesz łatwo dostosować to do systemu komunikacyjnego. W takim przypadku aktualizujesz tylko te dane, które zostały zmienione. W twoim komponencie Gravity większość obiektów będzie leżała na podłodze bez zmian, ale kilka z nich spada. Komponent Gravity ma upakowane tablice określające pozycję, prędkość, masę. Jeśli pozycja jest aktualizowana podczas pętli aktualizacji, dodaje się identyfikator jednostki i nową pozycję do listy zmian. Po zakończeniu przesyłasz te zmiany do dowolnego innego komponentu, który zachowuje wartość pozycji. Ta sama zasada, jeśli jakikolwiek inny komponent (na przykład komponent kontrolujący gracza) zmienia pozycję, przesyła nowe pozycje zmienionych podmiotów, komponent Gravity może go odsłuchiwać i aktualizować tylko te pozycje w swojej tablicy pozycji. Będziesz duplikować wiele danych, tak jak w poprzednim przykładzie, ale zamiast odnawiać wszystkie dane w każdym cyklu aktualizacji, aktualizujesz dane tylko po ich zmianie. Bardzo przydatne w sytuacjach, w których niewielkie ilości danych faktycznie zmieniają każdą klatkę, ale mogą stać się nieskuteczne, jeśli zmienią się duże ilości danych.
Więc nie ma srebrnej kuli. Istnieje wiele opcji. Najlepsze rozwiązanie zależy całkowicie od Twojej sytuacji, od twoich danych i sposobu ich przetwarzania. Może żaden z podanych przykładów nie jest dla ciebie odpowiedni, może wszystkie z nich są. Nie każdy komponent musi działać w taki sam sposób, niektórzy mogą korzystać z systemu zmiany/wiadomości, podczas gdy inni korzystają z opcji indeksów. Pamiętaj, że chociaż wiele wskazówek dotyczących wydajności DOD jest świetnych, jeśli potrzebujesz wydajności, przydaje się tylko w określonych sytuacjach. DOD nie polega na tym, by zawsze używać tablic, nie chodzi o maksymalizację wykorzystania pamięci podręcznej, powinieneś robić to tylko wtedy, gdy ma to naprawdę znaczenie. Profil profilu profilu. Poznaj swoje dane. Poznaj wzorce dostępu do danych. Znaj swoją architekturę (pamięć podręczną). Jeśli to wszystko zrobisz, rozwiązania staną się oczywiste, gdy będziesz o tym mówił :)
Mam nadzieję, że to pomoże!
wybierz konkretną wartość, jak -1, aby przedstawić lukę. – tp1
@ tp1, który działa, gdy przechowujesz pewne wartości, na przykład int dla punktów zdrowia na przykład. Jednak jeśli przechowujesz prędkość lub pozycję lub inne wartości za pomocą wartości zmiennoprzecinkowych lub atrybutów, które mogą mieć wartość 0 i wartość ujemną, to podejście nie działa. – Tobias
@ tp1 - i dlatego mamy wartości zerowe – Duniyadnd