Gute Frage. Soweit ich das beurteilen kann, gibt es keine einfache Lösung für dieses Problem. Es gibt mehrere Lösungen (von denen Sie einige bereits erwähnt haben), aber ich sehe keine sofortige Lösung.
Schauen wir uns zuerst das Ziel an. Das Ziel ist nicht, alle Daten in lineare Arrays zu bringen, sondern nur die Mittel, um das Ziel zu erreichen. Ziel ist die Optimierung der Performance durch Minimierung von Cache-Misses. Das ist alles. Wenn Sie OOP-Objekte verwenden, sind Ihre Entities-Daten von Daten umgeben, die Sie nicht unbedingt benötigen. Wenn Ihre Architektur eine Cache-Zeilengröße von 64 Bytes hat und Sie nur Gewicht (float), Position (vec3) und Geschwindigkeit (vec3) benötigen, verwenden Sie 28 Bytes, aber die restlichen 36 Bytes werden trotzdem geladen. Noch schlimmer ist, wenn diese 3 Werte nicht nebeneinander im Speicher liegen oder Ihre Datenstruktur eine Cache-Zeilengrenze überlappt, laden Sie mehrere Cache-Zeilen für nur 28 Bytes der tatsächlich verwendeten Daten.
Jetzt ist das nicht so schlimm, wenn Sie dies ein paar Mal tun. Selbst wenn Sie es hundert Mal tun, werden Sie es kaum bemerken. Wenn Sie dies jedoch tausend Mal pro Sekunde tun, kann dies zu einem Problem werden. Geben Sie also DOD ein, in dem Sie die Cache-Auslastung optimieren, indem Sie in der Regel lineare Arrays für jede Variable in Situationen erstellen, in denen lineare Zugriffsmuster vorhanden sind. In Ihrem Fall Arrays für Gewicht, Position, Geschwindigkeit. Wenn Sie die Position einer Entität laden, laden Sie erneut 64 Byte Daten. Da Ihre Positionsdaten jedoch in einem Array nebeneinander liegen, laden Sie nicht 1 Positionswert, sondern laden die Daten für 5 benachbarte Entitäten. Die nächste Iteration Ihrer Update-Schleife benötigt wahrscheinlich den nächsten Positionswert, der bereits in den Cache geladen wurde, usw., bis nur bei der 6. Entität neue Daten aus dem Hauptspeicher geladen werden müssen.
Das Ziel von DOD ist also nicht die Verwendung linearer Arrays, es maximiert die Cache-Auslastung, indem Daten platziert werden, auf die (ungefähr) gleichzeitig im Speicher zugegriffen wird. Wenn Sie fast immer auf 3 Variablen gleichzeitig zugreifen, müssen Sie nicht 3 Arrays für jede Variable erstellen, Sie können ebenso einfach eine Struktur erstellen, die nur diese 3 Werte enthält, und ein Array dieser Strukturen erstellen. Die beste Lösung hängt immer davon ab, wie Sie die Daten verwenden. Wenn Ihre Zugriffsmuster linear sind, Sie aber nicht immer alle Variablen verwenden, wählen Sie separate lineare Arrays. Wenn Ihre Zugriffsmuster unregelmäßiger sind, Sie aber immer alle Variablen gleichzeitig verwenden, setzen Sie sie zusammen in eine Struktur und erstellen Sie ein Array dieser Strukturen.
So gibt es Ihre Antwort in Kurzform: Es hängt alles von Ihrer Datennutzung ab. Aus diesem Grund kann ich Ihre Frage nicht direkt beantworten.Ich kann Ihnen einige Ideen geben, wie Sie mit Ihren Daten umgehen können, und Sie können selbst entscheiden, welches die nützlichsten (wenn es eines von ihnen gibt) in Ihrer Situation ist, oder vielleicht können Sie sie anpassen/mischen.
Sie können die am häufigsten verwendeten Daten in einem kontinuierlichen Array speichern. Zum Beispiel wird die Position oft von vielen verschiedenen Komponenten verwendet, daher ist sie ein idealer Kandidat für ein kontinuierliches Array. Gewicht wird dagegen nur von der Schwerkraftkomponente genutzt, so dass hier Lücken entstehen können. Sie haben den am häufigsten verwendeten Fall optimiert und erhalten weniger Leistung für weniger häufig verwendete Daten. Dennoch bin ich aus mehreren Gründen kein großer Fan dieser Lösung: Es ist immer noch ineffizient, Sie werden viel zu viele leere Daten laden, je niedriger das Verhältnis von # spezifischen Komponenten/# gesamten Einheiten ist, desto schlechter wird es. Wenn nur eine von acht Entitäten Schwerkraftkomponenten aufweist und diese Entitäten gleichmäßig über die Arrays verteilt sind, erhalten Sie für jede Aktualisierung immer noch einen Cache-Fehltreffer. Es nimmt auch an, dass alle Entitäten eine Position haben (oder was auch immer die allgemeine Variable ist), es ist schwer Entitäten hinzuzufügen oder zu entfernen, es ist unflexibel und einfach hässlich (imho sowieso). Es kann jedoch die einfachste Lösung sein.
Eine andere Möglichkeit, dies zu lösen, ist die Verwendung von Indizes. Jedes Array für eine Komponente wird gepackt, aber es gibt zwei zusätzliche Arrays, eins zum Abrufen der Entitäts-ID aus einem Komponenten-Array-Index und ein zweites zum Abrufen des Komponenten-Array-Index aus einer Entitäts-ID. Nehmen wir an, die Position wird von allen Entitäten geteilt, während Gewicht und Geschwindigkeit nur von Gravitation verwendet werden. Sie können jetzt über die gepackten Gewichtungs- und Geschwindigkeitsfelder iterieren und um die entsprechende Position zu erhalten/setzen, können Sie den gravityIndex -> entityID Wert erhalten, gehen Sie zur Position-Komponente, verwenden Sie die entityID -> positionIndex, um den korrekten Index zu erhalten Array positionieren. Der Vorteil ist, dass Ihre Zugriffe auf Gewicht und Geschwindigkeit nicht mehr zu Cache-Fehlern führen, aber Sie erhalten immer noch Cache-Fehler für die Positionen, wenn das Verhältnis zwischen # Schwerkraftkomponenten/# Positionskomponenten niedrig ist. Sie erhalten auch zusätzliche 2 Array-Lookups, aber ein 16-Bit unsigned int-Index sollte in den meisten Fällen ausreichen, damit diese Arrays gut in den Cache passen, was bedeutet, dass dies in den meisten Fällen keine sehr teure Operation ist. Trotzdem Profil Profil Profil, um sicher zu sein!
Eine dritte Option ist die Datenduplizierung. Nun, ich bin mir ziemlich sicher, dass das im Falle Ihrer Gravity-Komponente nicht die Mühe wert ist, ich denke, es ist in rechenintensiven Situationen interessanter, aber nehmen wir es als Beispiel. In diesem Fall verfügt die Gravity-Komponente über 3 gepackte Arrays für Gewicht, Geschwindigkeit und Position. Es hat auch eine ähnliche Indextabelle wie in der zweiten Option. Wenn Sie das Gravity-Komponentenupdate starten, aktualisieren Sie zuerst das Positionsarray vom ursprünglichen Positionsarray in der Positionskomponente, indem Sie die Indextabelle wie in Beispiel 2 verwenden. Jetzt haben Sie 3 gepackte Arrays, mit denen Sie linear mit maximalem Cache rechnen können Nutzung. Wenn Sie fertig sind, kopieren Sie die Position mithilfe der Indextabelle zurück in die ursprüngliche Positionskomponente. Nun, dies ist nicht schneller (in der Tat wahrscheinlich langsamer) als die zweite Option, wenn Sie es für etwas wie Schwerkraft verwenden, weil Sie Position nur einmal lesen und schreiben. Angenommen, Sie haben eine Komponente, in der Entitäten miteinander interagieren, wobei jeder Aktualisierungsdurchlauf mehrere Lese- und Schreibvorgänge erfordert, ist dies möglicherweise schneller. Alles hängt jedoch von Zugriffsmustern ab.
Die letzte Option, die ich erwähnen werde, ist ein auf Änderungen basierendes System. Sie können dies leicht in etwas wie ein Messaging-System anpassen. In diesem Fall aktualisieren Sie nur Daten, die geändert wurden. In Ihrer Schwerkraft-Komponente liegen die meisten Objekte auf dem Boden ohne Veränderung, aber einige fallen. Die Gravitätskomponente hat Arrays für Position, Geschwindigkeit und Gewicht gepackt. Wenn die Position während Ihrer Update-Schleife aktualisiert wird, fügen Sie die Entitäts-ID und die neue Position zu einer Änderungsliste hinzu. Wenn Sie fertig sind, senden Sie diese Änderungen an jede andere Komponente, die einen Positionswert enthält. Dasselbe Prinzip, wenn eine andere Komponente (zum Beispiel die Spielersteuerungskomponente) die Position ändert, sendet sie die neuen Positionen von geänderten Entitäten, die Schwerkraftkomponente kann dies hören und nur diese Positionen in ihrem Positionsarray aktualisieren. Sie werden viele Daten wie im vorherigen Beispiel duplizieren, aber anstatt alle Daten bei jedem Aktualisierungszyklus erneut zu lesen, aktualisieren Sie die Daten nur, wenn sie sich ändern. Sehr nützlich in Situationen, in denen kleine Datenmengen tatsächlich jeden Frame ändern, aber bei großen Datenmengen unwirksam werden.
So gibt es keine Wunderwaffe. Es gibt viele Möglichkeiten. Die beste Lösung hängt vollständig von Ihrer Situation, Ihren Daten und der Art ab, wie Sie diese Daten verarbeiten. Vielleicht ist keines der Beispiele, die ich gegeben habe, das Richtige für dich, vielleicht sind sie alle. Nicht jede Komponente muss auf die gleiche Weise funktionieren, manche verwenden das Change/Meldesystem, während andere die Option "Indizes" verwenden. Denken Sie daran, dass viele DOD-Leistungsrichtlinien zwar großartig sind, wenn Sie die Leistung benötigen, aber nur in bestimmten Situationen nützlich ist. Bei DOD geht es nicht darum, immer Arrays zu verwenden, es geht nicht immer darum, die Cache-Auslastung zu maximieren, sondern nur dort, wo es wirklich zählt. Profil Profil Profil. Kenne deine Daten. Kennen Sie Ihre Datenzugriffsmuster. Kennen Sie Ihre (Cache-) Architektur. Wenn Sie all das tun, werden Lösungen offensichtlich, wenn Sie darüber nachdenken :)
Hoffe, das hilft!
Wählen Sie einen bestimmten Wert wie -1, um eine Lücke darzustellen. – tp1
@ tp1 das funktioniert, wenn Sie einige Werte wie Ints für Gesundheitspunkte zum Beispiel speichern. Wenn Sie jedoch Geschwindigkeits- oder Positionswerte oder andere Werte mit Gleitkommazahlen oder Attributen speichern, die 0 oder negativ sein können, funktioniert diese Methode nicht. – Tobias
@ tp1 - und deshalb haben wir Nullen – Duniyadnd