Ich habe oft in den ECS-Game-Engine-Dokumentationen gelesen, dass eine gute Architektur ist, um den CPU-Cache mit Bedacht einzusetzen.
Ich kann mir jedoch nicht vorstellen, wie wir vom CPU-Cache profitieren können.
Wenn Komponenten in einem Array (oder einem Pool) im zusammenhängenden Speicher gespeichert werden, ist dies eine gute Möglichkeit, den CPU-Cache zu verwenden, ABER nur, wenn die Komponenten nacheinander gelesen werden.
Wenn wir Systeme verwenden, benötigen sie eine Entitätenliste, die eine Liste von Entitäten mit Komponenten mit bestimmten Typen enthält.
Diese Listen geben die Komponenten jedoch auf zufällige Weise und nicht nacheinander wieder.
Wie kann man ein ECS entwerfen, um den Cachetreffer zu maximieren?
EDIT:
Beispielsweise benötigt ein physisches System eine Entitätsliste für eine Entität mit den RigidBody- und Transform-Komponenten (es gibt einen Pool für RigidBody- und einen Pool für Transform-Komponenten).
Die Schleife zum Aktualisieren von Entitäten sieht also folgendermaßen aus:
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
}
Das Problem besteht darin, dass sich die RigidBody-Komponente von entity1 am Index 2 ihres Pools und die Tranform-Komponente von entity1 am Index 0 ihres Pools befinden kann (da einige Entitäten einige Komponenten haben können und nicht die andere, und weil Entitäten hinzugefügt / gelöscht werden / Komponenten zufällig).
Selbst wenn Komponenten im Arbeitsspeicher zusammenhängend sind, werden sie zufällig gelesen und haben daher mehr Cache-Fehler, oder?
Gibt es keine Möglichkeit, die nächsten Komponenten in der Schleife vorab abzurufen?
Antworten:
Mick Wests Artikel erklärt den vollständigen Prozess der Linearisierung von Daten von Entitätskomponenten. Bei der Tony Hawk-Serie funktionierte es vor Jahren mit viel weniger beeindruckender Hardware als heute, um die Leistung erheblich zu verbessern. Grundsätzlich verwendete er globale, vorab zugewiesene Arrays für jeden bestimmten Typ von Entitätsdaten (Position, Punktzahl und so weiter) und referenzierte jedes Array in einer bestimmten Phase seiner systemweiten
update()
Funktion. Sie können davon ausgehen, dass sich die Daten für jede Entität in jedem dieser globalen Arrays auf demselben Arrayindex befinden. Wenn also der Player zuerst erstellt wird, befinden sich die Daten möglicherweise[0]
in jedem Array unter.Noch spezifischer für die Cache-Optimierung sind die Folien von Christer Ericsson für C und C ++.
Um ein bisschen mehr Details zu geben, sollten Sie versuchen, zusammenhängende Speicherblöcke (am einfachsten als Arrays zuzuweisen) für jeden Datentyp (z. B. Position, xy und z) zu verwenden, um eine gute Referenzlokalität zu gewährleisten, wobei jeder dieser Datenblöcke einzeln verwendet wird
update()
Phasen aus Gründen der zeitlichen Lokalität, dh um sicherzustellen, dass der Cache nicht über den LRU-Algorithmus der Hardware geleert wird, bevor Sie innerhalb eines bestimmtenupdate()
Aufrufs Daten wiederverwenden, die Sie wiederverwenden möchten . Wie Sie bereits angedeutet haben, möchten Sie Ihre Entitäten und Komponenten nicht über als diskrete Objekte zuweisennew
, da dann Daten unterschiedlicher Typen auf jeder Entitätsinstanz verschachtelt werden, wodurch die Referenzlokalität verringert wird.Wenn Sie Abhängigkeiten zwischen Komponenten (Daten) haben, die es sich absolut nicht leisten können, einige Daten von den zugehörigen Daten zu trennen (z. B. Transformieren + Physik, Transformieren + Renderer), können Sie die Replikation von Transformationsdaten in den Arrays "Physik" und "Renderer" wählen Dies stellt sicher, dass alle relevanten Daten für jede leistungskritische Operation in die Cache-Zeilenbreite passen.
Denken Sie auch daran, dass der L2- und L3-Cache (sofern Sie dies für Ihre Zielplattform annehmen können) viel dazu beiträgt, die Probleme des L1-Caches, wie z. B. eine beschränkte Zeilenbreite, zu verringern. Dies sind also selbst bei einem L1-Fehler Sicherheitsnetze, die am häufigsten Callouts in den Hauptspeicher verhindern, der um Größenordnungen langsamer ist als Callouts in eine beliebige Cache-Ebene.
Hinweis zum Schreiben von Daten Das Schreiben wird nicht in den Hauptspeicher aufgerufen. Standardmäßig ist auf heutigen Systemen das Write-Back-Caching aktiviert: Beim Schreiben eines Werts wird dieser nur (anfangs) in den Cache und nicht in den Hauptspeicher geschrieben, sodass dies keine Engpässe mit sich bringt. Nur wenn Daten aus dem Hauptspeicher angefordert werden (dies geschieht nicht im Cache) und veraltet sind, wird der Hauptspeicher aus dem Cache aktualisiert.
quelle
std::vector
handelt sich im Grunde genommen um ein Array, dessen Größe sich dynamisch ändern lässt, und das daher auch zusammenhängend ist (de facto in älteren C ++ - Versionen und de jure in neueren C ++ - Versionen). Einige Implementierungen vonstd::deque
sind auch "zusammenhängend genug" (wenn auch nicht von Microsoft).