Installieren
Ich habe eine Entitätskomponentenarchitektur, in der Entitäten eine Reihe von Attributen haben können (die reine Daten ohne Verhalten sind), und es gibt Systeme, die die Entitätslogik ausführen, die auf diese Daten einwirken. Im Wesentlichen in etwas Pseudocode:
Entity
{
id;
map<id_type, Attribute> attributes;
}
System
{
update();
vector<Entity> entities;
}
Ein System, das sich nur mit einer konstanten Geschwindigkeit entlang aller Entitäten bewegt, könnte es sein
MovementSystem extends System
{
update()
{
for each entity in entities
position = entity.attributes["position"];
position += vec3(1,1,1);
}
}
Im Wesentlichen versuche ich, update () so effizient wie möglich zu parallelisieren. Dies kann erreicht werden, indem ganze Systeme parallel ausgeführt werden oder indem jedem Update () eines Systems mehrere Komponenten zugewiesen werden, damit verschiedene Threads das Update desselben Systems ausführen können, jedoch für eine andere Teilmenge von Entitäten, die bei diesem System registriert sind.
Problem
Im Fall des gezeigten MovementSystems ist die Parallelisierung trivial. Da Entitäten nicht voneinander abhängig sind und keine gemeinsam genutzten Daten ändern, können wir einfach alle Entitäten parallel verschieben.
Diese Systeme erfordern jedoch manchmal, dass Entitäten miteinander interagieren (Daten von / nach lesen / schreiben), manchmal innerhalb desselben Systems, aber häufig zwischen verschiedenen Systemen, die voneinander abhängen.
Beispielsweise können in einem Physiksystem Entitäten manchmal miteinander interagieren. Zwei Objekte kollidieren, ihre Positionen, Geschwindigkeiten und andere Attribute werden von ihnen gelesen, aktualisiert und dann werden die aktualisierten Attribute in beide Entitäten zurückgeschrieben.
Bevor das Rendering-System in der Engine mit dem Rendern von Entitäten beginnen kann, muss es warten, bis andere Systeme die Ausführung abgeschlossen haben, um sicherzustellen, dass alle relevanten Attribute den Anforderungen entsprechen.
Wenn wir versuchen, dies blind zu parallelisieren, führt dies zu klassischen Rennbedingungen, bei denen verschiedene Systeme gleichzeitig Daten lesen und ändern können.
Im Idealfall gibt es eine Lösung, bei der alle Systeme Daten von beliebigen Entitäten lesen können, ohne sich Sorgen machen zu müssen, dass andere Systeme dieselben Daten gleichzeitig ändern, und ohne dass der Programmierer sich um die ordnungsgemäße Anordnung der Ausführung und Parallelisierung von kümmert diese Systeme manuell (was manchmal gar nicht möglich ist).
In einer grundlegenden Implementierung könnte dies erreicht werden, indem einfach alle Datenlesevorgänge und -schreibvorgänge in kritischen Abschnitten abgelegt werden (wobei sie mit Mutexen geschützt werden). Dies führt jedoch zu einem hohen Laufzeitaufwand und ist wahrscheinlich nicht für leistungsempfindliche Anwendungen geeignet.
Lösung?
Meiner Meinung nach wäre eine mögliche Lösung ein System, bei dem das Lesen / Aktualisieren und Schreiben von Daten getrennt ist, sodass Systeme in einer teuren Phase nur Daten lesen und berechnen, was sie zum Berechnen benötigen, die Ergebnisse irgendwie zwischenspeichern und dann alle schreiben Die geänderten Daten werden in einem separaten Schreibdurchlauf an die Zielentitäten zurückgesendet. Alle Systeme würden auf die Daten in dem Zustand reagieren, in dem sie sich am Anfang des Rahmens befanden, und dann vor dem Ende des Rahmens, wenn alle Systeme die Aktualisierung abgeschlossen haben, wird ein serialisierter Schreibdurchlauf durchgeführt, bei dem die zwischengespeicherten Ergebnisse aus allen unterschiedlichen Ergebnissen resultieren Systeme werden durchlaufen und in die Zielentitäten zurückgeschrieben.
Dies basiert auf der (möglicherweise falschen?) Idee, dass der einfache Parallelisierungsgewinn groß genug sein könnte, um die Kosten (sowohl hinsichtlich der Laufzeitleistung als auch des Code-Overheads) für das Zwischenspeichern der Ergebnisse und den Schreibdurchlauf zu übertreffen.
Die Frage
Wie könnte ein solches System implementiert werden, um eine optimale Leistung zu erzielen? Was sind die Implementierungsdetails eines solchen Systems und was sind die Voraussetzungen für ein Entity-Component-System, das diese Lösung verwenden möchte?
quelle
Ich habe von einer interessanten Lösung für dieses Problem gehört: Die Idee ist, dass es 2 Kopien der Entitätsdaten geben würde (verschwenderisch, ich weiß). Eine Kopie wäre die gegenwärtige Kopie und die andere wäre die vergangene Kopie. Die vorliegende Kopie ist ausschließlich schreibgeschützt und die frühere Kopie ist ausschließlich schreibgeschützt. Ich gehe davon aus, dass Systeme nicht in dieselben Datenelemente schreiben möchten, aber wenn dies nicht der Fall ist, sollten sich diese Systeme im selben Thread befinden. Jeder Thread hätte Schreibzugriff auf die aktuellen Kopien sich gegenseitig ausschließender Abschnitte der Daten, und jeder Thread hat Lesezugriff auf alle früheren Kopien der Daten und kann somit die aktuellen Kopien unter Verwendung von Daten aus den früheren Kopien mit der Nummer 1 aktualisieren Verriegelung. Zwischen jedem Frame wird die aktuelle Kopie zur letzten Kopie, Sie möchten jedoch den Rollentausch übernehmen.
Diese Methode entfernt auch die Rennbedingungen, da alle Systeme mit einem veralteten Zustand arbeiten, der sich nicht ändert, bevor / nachdem das System ihn verarbeitet hat.
quelle
Ich kenne 3 Software-Designs, die die parallele Verarbeitung von Daten handhaben:
Hier einige Beispiele für jeden Ansatz, der in einem Entitätssystem verwendet werden kann:
CollisionSystem
, das liestPosition
undRigidBody
Komponenten enthält und a aktualisieren sollteVelocity
. Anstatt dasVelocity
direkt zu manipulieren , stellt derCollisionSystem
Wille stattdessen einCollisionEvent
in die Arbeitswarteschlange einesEventSystem
. Dieses Ereignis wird dann nacheinander mit anderen Aktualisierungen des verarbeitetVelocity
.EntitySystem
definiert eine Reihe von Komponenten, die gelesen und geschrieben werden müssen. Für jedenEntity
wird es eine aquire Lesesperre für jede Komponente , dass sie lesen möchte, und eine Schreibsperre für jede Komponente , dass sie aktualisieren möchte. Auf diese Weise kann jederEntitySystem
gleichzeitig Komponenten lesen, während Aktualisierungsvorgänge synchronisiert werden.MovementSystem
ist diePosition
Komponente unveränderlich und enthält eine Versionsnummer . DieMovementSystem
savely liest diePosition
undVelocity
Komponente und berechnet die neuePosition
, die Lese Inkrementieren Revisionsnummer und versucht , die AktualisierungPosition
Komponente. Im Falle einer gleichzeitigen Änderung gibt das Framework dies bei der Aktualisierung an undEntity
wird wieder in die Liste der Entitäten aufgenommen, die von der aktualisiert werden müssenMovementSystem
.Abhängig von den Systemen, Entitäten und Aktualisierungsintervallen kann jeder Ansatz gut oder schlecht sein. Ein Entity-System-Framework kann es dem Benutzer ermöglichen, zwischen diesen Optionen zu wählen, um die Leistung zu optimieren.
Ich hoffe, ich konnte der Diskussion einige Ideen hinzufügen und bitte lassen Sie mich wissen, wenn es Neuigkeiten dazu gibt.
quelle