C++ - Organisation/Verwaltung von millionen Objekten
C++ - Organisation/Verwaltung von millionen Objekten
Hallo.
Mein Projekt verfügt über ein Hauptobjekt Welt. Diese Welt hat eine oder mehrere Karten. In den Karten gibt es Objekte. Objekte können weitere Objekte beinhalten, z.B. wenn das Objekt ein Behälter (Kiste, Beutel, Truhe, usw.) ist.
Die "Haupthierarchie" ist daher sehr einfach, wie vermutlich bei den meisten Programmen, die eine Welt/Spielwelt darstellen möchten: Welt -> Karte/Level -> Objekt -> Objekt -> usw..
Ein Objekt verfügt direkt nur über die Eigenschaften/Attribute, die zu 99% in allen Objekten vorkommen, wie z.B. Größe (XYZ) und Gewicht (gramm). Alle weiteren Informationen, Definitionen und Beschreibungen werden dem Objekt direkt zugeordnet, bzw. verfügt das Objekt über Möglichkeiten, diese Informationen zu speichern. Es gibt zwar Basisobjekte, die in erster Linie dazu dienen ein Objekt zu erzeugen, aber in der Regel halten die Objekte die meisten Informationen nochmal/redundant. Durch diese Redundanz kann ich z.B. zwei Objekte durch das gleiche Basisobjekt erstellen, aber danach individuell anpassen. Ich hatte anfangs versucht, diese Redundanz zu vermeiden, indem ich z.B. bestimmte Daten erst "verdopple" wenn ich sie anpassen möchte, aber das hat die ganze Sache extrem verkompliziert und war letzten Endes nicht schneller, verbrauchte nur weniger Speicher.
Die Objekte und dazugehörigen Daten/Informationen werden alle durch einen Speichermanager erzeugt und verwaltet. Jede Klasse hat einen Verweis/Zeiger/Weakpointer auf die Welt, wo der Speichermanager "liegt". Fällt der Referenzzähler auf 0, wird eine Delete-Funktion aufgerufen, diese ruft dann die entsprechende Freigabefunktion/Free von der Welt auf. Die Freigabefunktion führt den Destruktor von der freizugebenden Instanz aus und erledigt die weiteren Arbeiten, um den Speicher freizugeben.
Diese Vorgehensweise macht die ganze Angelegenheit extrem schnell, ich teste das immer mit den Daten der Spieleklassiker Ultima 7 und Serpent Isle. (https://www.gog.com/game/ultima_7_complete)
Für Ultima 7 werden beim Laden der Spielwelt 3.361.485 Klassen/Instanzen erstellt und miteinander verknüpft. Bei Serpent Isle sind es 3.956.333 Klassen/Instanzen. In beiden Fällen ist die gesamte Welt in wenigen Sekunden geladen. Das Objekt-System ist noch in der Entwicklung und ich gehe davon aus, dass in der finalen Version über 5.000.000 Klassen/Instanzen erstellt werden.
Das verwendete System ist zwar extrem schnell, hat aber auch seine Nachteile, wie z.B. dass der Speichermanager und die Welt/Karte/Objekte nicht threadsafe sind. Ich muss, sobald ich Daten lesen/ändern/erstellen/freigeben möchte, die Welt sperren (CriticalSection).
Ich kann mir aber auch nicht vorstellen, dass es Programme mit vielen Daten gibt, die multithreaded sind, aber die Daten ohne einen zentralen Sperrmechanismus bearbeiten. Ich habe das zwar nicht getestet, aber ich gehe davon aus, dass wenn ich z.B. die Daten und Speichermanager threadsafe mache, die Performance hart in den Keller geht.
Habt ihr eine Idee wie ich das besser lösen könnte?
Mein Projekt verfügt über ein Hauptobjekt Welt. Diese Welt hat eine oder mehrere Karten. In den Karten gibt es Objekte. Objekte können weitere Objekte beinhalten, z.B. wenn das Objekt ein Behälter (Kiste, Beutel, Truhe, usw.) ist.
Die "Haupthierarchie" ist daher sehr einfach, wie vermutlich bei den meisten Programmen, die eine Welt/Spielwelt darstellen möchten: Welt -> Karte/Level -> Objekt -> Objekt -> usw..
Ein Objekt verfügt direkt nur über die Eigenschaften/Attribute, die zu 99% in allen Objekten vorkommen, wie z.B. Größe (XYZ) und Gewicht (gramm). Alle weiteren Informationen, Definitionen und Beschreibungen werden dem Objekt direkt zugeordnet, bzw. verfügt das Objekt über Möglichkeiten, diese Informationen zu speichern. Es gibt zwar Basisobjekte, die in erster Linie dazu dienen ein Objekt zu erzeugen, aber in der Regel halten die Objekte die meisten Informationen nochmal/redundant. Durch diese Redundanz kann ich z.B. zwei Objekte durch das gleiche Basisobjekt erstellen, aber danach individuell anpassen. Ich hatte anfangs versucht, diese Redundanz zu vermeiden, indem ich z.B. bestimmte Daten erst "verdopple" wenn ich sie anpassen möchte, aber das hat die ganze Sache extrem verkompliziert und war letzten Endes nicht schneller, verbrauchte nur weniger Speicher.
Die Objekte und dazugehörigen Daten/Informationen werden alle durch einen Speichermanager erzeugt und verwaltet. Jede Klasse hat einen Verweis/Zeiger/Weakpointer auf die Welt, wo der Speichermanager "liegt". Fällt der Referenzzähler auf 0, wird eine Delete-Funktion aufgerufen, diese ruft dann die entsprechende Freigabefunktion/Free von der Welt auf. Die Freigabefunktion führt den Destruktor von der freizugebenden Instanz aus und erledigt die weiteren Arbeiten, um den Speicher freizugeben.
Diese Vorgehensweise macht die ganze Angelegenheit extrem schnell, ich teste das immer mit den Daten der Spieleklassiker Ultima 7 und Serpent Isle. (https://www.gog.com/game/ultima_7_complete)
Für Ultima 7 werden beim Laden der Spielwelt 3.361.485 Klassen/Instanzen erstellt und miteinander verknüpft. Bei Serpent Isle sind es 3.956.333 Klassen/Instanzen. In beiden Fällen ist die gesamte Welt in wenigen Sekunden geladen. Das Objekt-System ist noch in der Entwicklung und ich gehe davon aus, dass in der finalen Version über 5.000.000 Klassen/Instanzen erstellt werden.
Das verwendete System ist zwar extrem schnell, hat aber auch seine Nachteile, wie z.B. dass der Speichermanager und die Welt/Karte/Objekte nicht threadsafe sind. Ich muss, sobald ich Daten lesen/ändern/erstellen/freigeben möchte, die Welt sperren (CriticalSection).
Ich kann mir aber auch nicht vorstellen, dass es Programme mit vielen Daten gibt, die multithreaded sind, aber die Daten ohne einen zentralen Sperrmechanismus bearbeiten. Ich habe das zwar nicht getestet, aber ich gehe davon aus, dass wenn ich z.B. die Daten und Speichermanager threadsafe mache, die Performance hart in den Keller geht.
Habt ihr eine Idee wie ich das besser lösen könnte?
- Krishty
- Establishment
- Beiträge: 8316
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: C++ - Organisation/Verwaltung von millionen Objekten
Was willst du denn multithreaded machen? Rendering dürfte kein Problem sein, das manipuliert/löscht ja keine Objekte. Gameplay? Physik?Goderion hat geschrieben:Ich kann mir aber auch nicht vorstellen, dass es Programme mit vielen Daten gibt, die multithreaded sind, aber die Daten ohne einen zentralen Sperrmechanismus bearbeiten. Ich habe das zwar nicht getestet, aber ich gehe davon aus, dass wenn ich z.B. die Daten und Speichermanager threadsafe mache, die Performance hart in den Keller geht.
Habt ihr eine Idee wie ich das besser lösen könnte?
Re: C++ - Organisation/Verwaltung von millionen Objekten
Im Mainthread werden die Daten beim Verarbeiten der GUI-Nachrichten genutzt.Krishty hat geschrieben:Was willst du denn multithreaded machen? Rendering dürfte kein Problem sein, das manipuliert/löscht ja keine Objekte. Gameplay? Physik?
1. Situation - Rendering: Die Welt wird gesperrt und es werden die nötigen Daten gesammelt und an ein Rendersystem übermittelt, dass die Daten in einer Kommandoliste so ablegt, dass nur noch Grafikinformationen vorhanden sind und keine Verweise mehr auf die Weltdaten erforderlich sind. Ein andere Thread nimmt dann diese Daten, verarbeitet diese weiter und gibt sie letzten Endes an Direct3D9 weiter.
2. Situation - Benutzereingaben: Der Benutzer verändert ständig die Weltdaten, entweder durch den Editor oder als Spieler.
Im Logikthread werden die Daten bezüglich Gameplay, Physik und KI verarbeitet, was wie bei den Benutzereingaben zu alle möglichen Änderungen führen kann.
Es gibt also mindestens 2 voneinander unabhängig laufende Threads, die die gleichen Daten lesen und manipulieren, was eine Zugriffskontrolle unabdingbar macht.
Ich könnte die Logik auch im gleichen Thread/Mainthread laufen lassen, aber ich erhoffe mir dadurch eine besser Performance, vor allem wenn die Logik viel berechnen muss (Pathfinding, etc.). Das ist ja auch der Grund und Nutzen von Multithreading.
- RustySpoon
- Establishment
- Beiträge: 298
- Registriert: 17.03.2009, 13:59
- Wohnort: Dresden
Re: C++ - Organisation/Verwaltung von millionen Objekten
Das wird man so pauschal nicht beantworten können. Das hängt sehr stark davon ab, wie du deine Welt organsierst und was da wann simuliert/aktualisiert wird. In der Regel wirst du ja nicht bei jedem Update über alle Objekte drüber rennen und die volle Latte an Physik/AI/etc. berechnen, sondern das irgendwie in Abhängigkeit von Sichtbarkeit/Entfernung zum Spieler/etc. vereinfachen.
Angenommen, du weißt welche Objekte zwingend aktualisiert werden müssen, dann wäre in der Umsetzung das eine Extrem immer die komplette Welt zu sperren, das andere immer jedes Objekt einzeln. Ersteres führt dazu, dass Multithreading praktisch nutzlos ist, letzteres induziert einen riesigen Speicher- und Verwaltungsoverhead. Ergo wirst du irgendein Mittelding fahren wollen, wo immer hinreichend große Objektgruppen auf einmal gesperrt werden. Aber bei der Größe und Zusammensetzung dieser Objektgruppen wirst du um viel experimentieren und messen nicht drumherum kommen.
Angenommen, du weißt welche Objekte zwingend aktualisiert werden müssen, dann wäre in der Umsetzung das eine Extrem immer die komplette Welt zu sperren, das andere immer jedes Objekt einzeln. Ersteres führt dazu, dass Multithreading praktisch nutzlos ist, letzteres induziert einen riesigen Speicher- und Verwaltungsoverhead. Ergo wirst du irgendein Mittelding fahren wollen, wo immer hinreichend große Objektgruppen auf einmal gesperrt werden. Aber bei der Größe und Zusammensetzung dieser Objektgruppen wirst du um viel experimentieren und messen nicht drumherum kommen.
- Schrompf
- Moderator
- Beiträge: 5077
- Registriert: 25.02.2009, 23:44
- Benutzertext: Lernt nur selten dazu
- Echter Name: Thomas
- Wohnort: Dresden
- Kontaktdaten:
Re: C++ - Organisation/Verwaltung von millionen Objekten
Zusätzlich zum "Kann man pauschal so nicht sagen" will ich hinzufügen: Du musst kleiner granularisieren. Das Rendering z.B. ist eine rein lesende Operation auf der Spielwelt. Du könntest also z.B. stressfrei 8 oder wasweißichwieviele Jobs starten, die gleichzeitig über disjunkte Teile der Welt iterieren und die DrawCalls einsammeln. Das Cullen kann man parallelisieren. Mit DX11/DX12/Vulkan/Mantle kann man selbst das Rendern parallelisieren. Daten-Parallele Aufgaben haben immer den Vorteil, dass sie viel besser mit der Hardware skalieren als themen-parallele Aufgaben.
Ansonsten: ja, new ist schnell. Ein paar Millionen Allokationen pro Sekunde sind machbar. Für mich damals überraschend war die Erkenntnis, dass das dazugehörige delete deutlich langsamer ist.
Evtl. wäre es für solche Jobs aber besser, zumindest während der initialen Einrichtung einen Custom Allokator einzusetzen, der linear aus einem großen Speicherblock schöpft. Das hat zusätzlich den Vorteil, dass alle Daten schön nah beieinander liegen und damit cache-freundlicher sind. Aber wenn Du viel auf virtuelle Funktionen setzt, ist Dir Cache-Freundlichkeit wahrscheinlich eh nicht so wichtig.
Und poste mal Screenshots, wenn's was zu zeigen gibt :-)
Ansonsten: ja, new ist schnell. Ein paar Millionen Allokationen pro Sekunde sind machbar. Für mich damals überraschend war die Erkenntnis, dass das dazugehörige delete deutlich langsamer ist.
Evtl. wäre es für solche Jobs aber besser, zumindest während der initialen Einrichtung einen Custom Allokator einzusetzen, der linear aus einem großen Speicherblock schöpft. Das hat zusätzlich den Vorteil, dass alle Daten schön nah beieinander liegen und damit cache-freundlicher sind. Aber wenn Du viel auf virtuelle Funktionen setzt, ist Dir Cache-Freundlichkeit wahrscheinlich eh nicht so wichtig.
Und poste mal Screenshots, wenn's was zu zeigen gibt :-)
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
- Krishty
- Establishment
- Beiträge: 8316
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: C++ - Organisation/Verwaltung von millionen Objekten
Ich wollte auch erst RustySpoons Extrem vorschlagen: Jedes Objekt einzeln locken und alle Threads drauf loslassen. 99,9 % der Zeit werden Rendering und KI nicht auf das selbe Objekt zugreifen, und da ist ein SpinLock (idealerweise nicht aufwändiger implementiert als atomic<char>) von der Zugriffszeit her so gut wie kostenlos. Ich sehe nur zwei Probleme:
1. Bei 5 mio Objekten macht das 5 MiB zusätzliche Cache-Belastung pro Frame, schlägt also schon auf den Magen.
2. Objekte können zyklische Abhängigkeiten haben (je nachdem, wie deine KI/Physik funktionieren) und du würdest dann zwangsläufig in das Problem rekursiver Locks laufen, und das ist hässlich. (Andererseits ist Re-entrancy in Spiellogik auch ohne Multi-Threading schön ein hässliches Problem.)
1. Bei 5 mio Objekten macht das 5 MiB zusätzliche Cache-Belastung pro Frame, schlägt also schon auf den Magen.
2. Objekte können zyklische Abhängigkeiten haben (je nachdem, wie deine KI/Physik funktionieren) und du würdest dann zwangsläufig in das Problem rekursiver Locks laufen, und das ist hässlich. (Andererseits ist Re-entrancy in Spiellogik auch ohne Multi-Threading schön ein hässliches Problem.)
- Schrompf
- Moderator
- Beiträge: 5077
- Registriert: 25.02.2009, 23:44
- Benutzertext: Lernt nur selten dazu
- Echter Name: Thomas
- Wohnort: Dresden
- Kontaktdaten:
Re: C++ - Organisation/Verwaltung von millionen Objekten
Naja, und bei Spielen würde man z.B. ne KI schreiben, indem man die nahesten x Feinde sucht und eine Bewegung nach deren Position ermittelt. Das wäre dann der Zugriff auf State von anderen Objekten, während das andere Objekt potentiell gerade selbst agiert. Battlefield hat (ab der 4, glaube ich) darum jeden internen State von InGame Entities gedoppelt und betreibt Double Buffering von Frame zu Frame. Entities zur Laufzeit zu erzeugen und zu löschen ist ein lösbares Problem - im einfachsten Fall speichert man einfach solche Aktionen und führt sie erst am Ende des Update Cyles aus, oder am Anfang des nächsten. Musste ich eh schon immer machen, auch ohne meine Spiellogik tatsächlich zu parallelisieren, aber ich habe die Gründe vergessen.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Re: C++ - Organisation/Verwaltung von millionen Objekten
Vielen Dank für die Antworten!
Die Objekte die für das Rendering benötigt oder durch Benutzereingaben verändert werden könnten, lassen sich ja auf der Karte recht einfach geographisch ermitteln, jetzt muss ich mir überlegen, wie ich eine partielle Sperrung realisieren könnte.
Der Renderer hat gerade alle Objekte eingesammelt, die gezeichnet werden müssen. Zur gleichen Zeit wird aber eines von den Objekten durch den Logikthread gelöscht.
Die Objekte in einem Container werden durch eine Liste gehalten. Wenn jetzt der Renderer gerade die Liste durchgeht und währenddessen der Logikthread ein Objekt aus der Liste löscht oder hinzufügt, kann es zu Fehlern führen.
new hat den großen Vorteil, dass es threadsafe ist und die Erstellung und Zerstörung der Instanzen quasi unabhängig von allem anderen durchgeführt werden kann.
Ein Speichermanager ist nicht threadsafe und die Objekte, die durch ihn verwaltet werden, benötigen alle einen kleinen Mehraufwand in der Entwicklung.
Aus diversen Gründen (Sicherheit, Analyse, Validierung, Debugging) brauche ich für jede Klasse/Objektart einen Verwalter, der die Objekte erzeugt und löscht. Ein Objekt/Instanz darf nicht außerhalb des Verwalters erstellt oder gelöscht werden.
Diese Bedingung macht so gut wie alle Vorteile von new gegenüber einem eigenen Speichermanager zunichte und dann sehe ich keinen Grund mehr, auf diesen zu verzichten, zumal er auch deutlich schneller ist.
Virtuelle Funktionen "sollten" eigentlich bis auf die Delete-Funktion, nicht vorkommen, aber aus irgendeinem Grund werden AddRef und Release völlig sinnlos per vtable aufgerufen.
Die Objekte werden folgendermaßen definiert:
Eigentlich dürfte er beim Aufruf von AddRef oder Release nicht den vtable heranziehen, außer man macht es durch einen Zeiger vom Typ InterfaceObject.
InterfaceObjekt muss aber weiter bestehen, es gibt noch andere Basis-Objekt-Klassen, die von InterfaceObject erben, wie z.B. eine Version mit thread-sicherem Referenzzähler.
Wenn ich keinen Weg finde, dem Compiler diesen Unfug auszutreiben, werde ich für die Spieldaten-Objekte einen extra BasisObjekt/ClassObject programmieren, InterfaceObject sollte für die eigentlich eh nicht nötig sein.
Bei Ultima 7 befinden sich z.B. 237.886 Objekte auf der Karte/Level, die alles jedes mal durchzugehen wäre wirklich Wahnsinn. Es gibt spezielle Strukturen/Klassen, mit der die Logik verwaltet wird. Für jede Aktion gibt es einen Prozess, der dann in jedem Logik-Frame bis zur Vollendung ausgeführt/fortgesetzt wird.RustySpoon hat geschrieben:Das wird man so pauschal nicht beantworten können. Das hängt sehr stark davon ab, wie du deine Welt organsierst und was da wann simuliert/aktualisiert wird. In der Regel wirst du ja nicht bei jedem Update über alle Objekte drüber rennen und die volle Latte an Physik/AI/etc. berechnen, sondern das irgendwie in Abhängigkeit von Sichtbarkeit/Entfernung zum Spieler/etc. vereinfachen.
Auch beim Sperren der gesamten Welt sollte meinem Plan zufolge Multithreading nicht nutzlos sein. Der Logigthread soll ähnlich wie beim Rendering wenn möglich erstmal die nötigen Weltdaten sammeln, die Sperrung (CriticalSection verlassen) aufheben, "Zeug" berechnen, wieder sperren und Änderungen vornehmen. Wenn der Logikthread beim gesamten Prozess die Welt sperren würde, dann wäre ein extra Thread dafür wirklich nutzlos.RustySpoon hat geschrieben:Angenommen, du weißt welche Objekte zwingend aktualisiert werden müssen, dann wäre in der Umsetzung das eine Extrem immer die komplette Welt zu sperren, das andere immer jedes Objekt einzeln. Ersteres führt dazu, dass Multithreading praktisch nutzlos ist, letzteres induziert einen riesigen Speicher- und Verwaltungsoverhead.
Ich werde das wohl testen müssen, wenn alle Aktionen/Prozesse fertig sind und schauen, ob ein extra Thread "reicht", um trotz einer kompletten Sperrung durch Multithreading einen Performancebonus rauszuholen.RustySpoon hat geschrieben:Ergo wirst du irgendein Mittelding fahren wollen, wo immer hinreichend große Objektgruppen auf einmal gesperrt werden. Aber bei der Größe und Zusammensetzung dieser Objektgruppen wirst du um viel experimentieren und messen nicht drumherum kommen.
Die Objekte die für das Rendering benötigt oder durch Benutzereingaben verändert werden könnten, lassen sich ja auf der Karte recht einfach geographisch ermitteln, jetzt muss ich mir überlegen, wie ich eine partielle Sperrung realisieren könnte.
Die Aussage von Krishty hat mich schon stutzig gemacht. Auch wenn nur ein Thread die Daten ändert und ein anderer Thread die Daten nur liest, kann es doch ohne eine Sperrung zu unvorhersehbaren Fehlern führen?Schrompf hat geschrieben:Zusätzlich zum "Kann man pauschal so nicht sagen" will ich hinzufügen: Du musst kleiner granularisieren. Das Rendering z.B. ist eine rein lesende Operation auf der Spielwelt. Du könntest also z.B. stressfrei 8 oder wasweißichwieviele Jobs starten, die gleichzeitig über disjunkte Teile der Welt iterieren und die DrawCalls einsammeln. Das Cullen kann man parallelisieren. Mit DX11/DX12/Vulkan/Mantle kann man selbst das Rendern parallelisieren. Daten-Parallele Aufgaben haben immer den Vorteil, dass sie viel besser mit der Hardware skalieren als themen-parallele Aufgaben.
Der Renderer hat gerade alle Objekte eingesammelt, die gezeichnet werden müssen. Zur gleichen Zeit wird aber eines von den Objekten durch den Logikthread gelöscht.
Die Objekte in einem Container werden durch eine Liste gehalten. Wenn jetzt der Renderer gerade die Liste durchgeht und währenddessen der Logikthread ein Objekt aus der Liste löscht oder hinzufügt, kann es zu Fehlern führen.
Dazu habe ich mir auch viele Gedanken gemacht, inwiefern ein eigener Speichermanager wirklich sinnvoll ist.Schrompf hat geschrieben:Ansonsten: ja, new ist schnell. Ein paar Millionen Allokationen pro Sekunde sind machbar. Für mich damals überraschend war die Erkenntnis, dass das dazugehörige delete deutlich langsamer ist.
new hat den großen Vorteil, dass es threadsafe ist und die Erstellung und Zerstörung der Instanzen quasi unabhängig von allem anderen durchgeführt werden kann.
Ein Speichermanager ist nicht threadsafe und die Objekte, die durch ihn verwaltet werden, benötigen alle einen kleinen Mehraufwand in der Entwicklung.
Aus diversen Gründen (Sicherheit, Analyse, Validierung, Debugging) brauche ich für jede Klasse/Objektart einen Verwalter, der die Objekte erzeugt und löscht. Ein Objekt/Instanz darf nicht außerhalb des Verwalters erstellt oder gelöscht werden.
Diese Bedingung macht so gut wie alle Vorteile von new gegenüber einem eigenen Speichermanager zunichte und dann sehe ich keinen Grund mehr, auf diesen zu verzichten, zumal er auch deutlich schneller ist.
Mit Custom Allokator ist gemeint, das man den new operator überschreibt? Inwiefern kann ich so die Speicherreservierung beschleunigen? Die Objekte können vielleicht Anfangs auf einem großen Speicherblock kommen, aber wie lösche ich danach ein Einzelnes aus diesem Block?Schrompf hat geschrieben:Evtl. wäre es für solche Jobs aber besser, zumindest während der initialen Einrichtung einen Custom Allokator einzusetzen, der linear aus einem großen Speicherblock schöpft. Das hat zusätzlich den Vorteil, dass alle Daten schön nah beieinander liegen und damit cache-freundlicher sind.
Die Objekte liegen schon durch den Speichermanager, der intern mit Blöcken arbeitet, ebenfalls dicht beieinander, quasi in einer Reihe pro Block.Schrompf hat geschrieben:Aber wenn Du viel auf virtuelle Funktionen setzt, ist Dir Cache-Freundlichkeit wahrscheinlich eh nicht so wichtig.
Virtuelle Funktionen "sollten" eigentlich bis auf die Delete-Funktion, nicht vorkommen, aber aus irgendeinem Grund werden AddRef und Release völlig sinnlos per vtable aufgerufen.
Die Objekte werden folgendermaßen definiert:
Code: Alles auswählen
class InterfaceObject
{
public:
virtual Void AddRef(Void) = 0;
virtual Void Release(Void) = 0;
};
class ClassObject : public InterfaceObject
{
public:
ClassObject(Void)
{
m_ReferenceCount = 0;
}
Void AddRef(Void)
{
++m_ReferenceCount;
}
Void Release(Void)
{
--m_ReferenceCount;
if (0 == m_ReferenceCount)
{
Delete();
}
}
private:
virtual Void Delete(Void) = 0;
Int32 m_ReferenceCount;
};
class ClassGameObject : public ClassObject
{
private:
Void Delete(Void)
{
}
};
typedef TemplateWeakPointer2<ClassGameObject> wpGameObject;
typedef TemplateSmartPointer2<ClassGameObject> spGameObject;
InterfaceObjekt muss aber weiter bestehen, es gibt noch andere Basis-Objekt-Klassen, die von InterfaceObject erben, wie z.B. eine Version mit thread-sicherem Referenzzähler.
Wenn ich keinen Weg finde, dem Compiler diesen Unfug auszutreiben, werde ich für die Spieldaten-Objekte einen extra BasisObjekt/ClassObject programmieren, InterfaceObject sollte für die eigentlich eh nicht nötig sein.
Die Welt wird schon bis auf wenige Fehler korrekt gerendert, ist aber halt Ultima 7, bzw. Serpent Isle. Hab mal einen Screenshot in den Anhang gepackt.Schrompf hat geschrieben:Und poste mal Screenshots, wenn's was zu zeigen gibt :-)
- Krishty
- Establishment
- Beiträge: 8316
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: C++ - Organisation/Verwaltung von millionen Objekten
Was Custom Allocators angeht: Windows stellt via HeapCreate() Speicher-Manager zur Verfügung. Die sind thread-safe, low-fragmentation, unterstützen alloc/realloc/free, und sind 100 % kompatibel zu Application Verifier & Co. (Debugging-Traum).
Sie sind nicht ganz so platzeffizient wie ein eigener Manager, der aus einem Block schöpft, aber sie sind bereits fertig da. (Schonmal einen Page Heap selber programmiert? Zum Haareraufen.)
Wenn man dem Level einen eigenen Heap gibt, dauert das Freigeben keine 100k Takte (natürlich nur, so lange man den Heap direkt zerstört, und nicht die Objekte darauf einzeln ;) )
Sie sind nicht ganz so platzeffizient wie ein eigener Manager, der aus einem Block schöpft, aber sie sind bereits fertig da. (Schonmal einen Page Heap selber programmiert? Zum Haareraufen.)
Wenn man dem Level einen eigenen Heap gibt, dauert das Freigeben keine 100k Takte (natürlich nur, so lange man den Heap direkt zerstört, und nicht die Objekte darauf einzeln ;) )
Re: C++ - Organisation/Verwaltung von millionen Objekten
Ah, mich schon immer gefragt, wozu man einen "extra" Heap per HeapCreate erzeugen sollte, aber nie genauer geguckt was man damit eigentlich alles machen kann.
Wegen dem sinnlosen benutzen vom vtable, was tut der Compiler da nur? wenn ich AddRef und Release mit __forceinline versehe, dann wird in der Release-Version der vtable nicht mehr genutzt.
Wegen dem sinnlosen benutzen vom vtable, was tut der Compiler da nur? wenn ich AddRef und Release mit __forceinline versehe, dann wird in der Release-Version der vtable nicht mehr genutzt.
- Schrompf
- Moderator
- Beiträge: 5077
- Registriert: 25.02.2009, 23:44
- Benutzertext: Lernt nur selten dazu
- Echter Name: Thomas
- Wohnort: Dresden
- Kontaktdaten:
Re: C++ - Organisation/Verwaltung von millionen Objekten
Jupp. Am häufigsten sind die logischen Fehler, wenn halt mehrere Leser auf die Daten zugreifen, und jeder Leser potentiell andere Ergebnisse bekommen kann. Wenn Du das Ding dann nur an ner anderen Stelle renderst, ist das unkritisch. Wenn sich daraus KI-Verhalten ableitet oder die Kollisionsprüfung abweichende Ergebnisse liefert, kann das richtig Ärger geben.Goderion hat geschrieben: Die Aussage von Krishty hat mich schon stutzig gemacht. Auch wenn nur ein Thread die Daten ändert und ein anderer Thread die Daten nur liest, kann es doch ohne eine Sperrung zu unvorhersehbaren Fehlern führen?
Und seltener, aber ebenso möglich, sind direkte Lesefehler: Der Lese-Thread liest die Zahl und bekommt einen Wert, der völliger Quatsch ist. Also z.B. war ein Int vorher bei 0x0000ffe7 und würde danach 0x00010013 haben. x86 (und auch sonst niemand, glaube ich) garantiert nicht, dass die 4Byte des Integers atomar gelesen werden. Du könntest also, wenn der eine Core liest, während der andere schreibt, auch theoretisch ne 0x0001ffe7 lesen, also zwei Byte des alten Wertes und zwei Byte des neuen Wertes. Und das würde man selbst beim simplen Rendering sehen, wenn das Sprite plötzlich einen Meter im Off wäre.
Joa, aber wie gesagt: verzögertes Erstellen/Löschen musste ich selbst in meinen Single-Threaded-Spielen schon einbauen. Habe aber wie gesagt vergessen, warum ich das tun musste.Der Renderer hat gerade alle Objekte eingesammelt, die gezeichnet werden müssen. Zur gleichen Zeit wird aber eines von den Objekten durch den Logikthread gelöscht.
Die Objekte in einem Container werden durch eine Liste gehalten. Wenn jetzt der Renderer gerade die Liste durchgeht und währenddessen der Logikthread ein Objekt aus der Liste löscht oder hinzufügt, kann es zu Fehlern führen.
Wie gesagt: das kannst Du machen, wie Du möchtest. Ich empfände es als unangenehm, nix mehr auf dem Stack anlegen zu können. Da kannst Du auch gleich Java schreiben.Aus diversen Gründen (Sicherheit, Analyse, Validierung, Debugging) brauche ich für jede Klasse/Objektart einen Verwalter, der die Objekte erzeugt und löscht. Ein Objekt/Instanz darf nicht außerhalb des Verwalters erstellt oder gelöscht werden.
Diese Bedingung macht so gut wie alle Vorteile von new gegenüber einem eigenen Speichermanager zunichte und dann sehe ich keinen Grund mehr, auf diesen zu verzichten, zumal er auch deutlich schneller ist.
Ich meine einen Custom Allocator, wie Du ihn z.B. an std::vector<> übergeben kannst. Man kann auch den globalen new-Operator überschreiben, aber pro Klasse jedesmal den new-Operator zu überschreiben finde ich mühsam. Und den könnte man dann im Burst-Modus laufen lassen:Mit Custom Allokator ist gemeint, das man den new operator überschreibt? Inwiefern kann ich so die Speicherreservierung beschleunigen? Die Objekte können vielleicht Anfangs auf einem großen Speicherblock kommen, aber wie lösche ich danach ein Einzelnes aus diesem Block?
Code: Alles auswählen
void* alloc() {
if( istBeimLadenDerWelt )
return einStückAusDemGroßenBlob;
else
return HeapAlloc(...);
}
void free(void* p) {
if( p ist ImGroßenBlob )
// nix zu tun
else
return HeapFree(...);
}
Ist doch cool! Willst Du das Spiel mal komplett nachbauen oder soll das ein reiner Viewer bleiben?Die Welt wird schon bis auf wenige Fehler korrekt gerendert, ist aber halt Ultima 7, bzw. Serpent Isle. Hab mal einen Screenshot in den Anhang gepackt.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Re: C++ - Organisation/Verwaltung von millionen Objekten
Mmmh... ich glaube ich habe da eine grobe Idee. Ich könnte ein System entwickeln, dass das von dir erwähnte verzögerte löschen/verändern der Daten realisiert.Schrompf hat geschrieben:Joa, aber wie gesagt: verzögertes Erstellen/Löschen musste ich selbst in meinen Single-Threaded-Spielen schon einbauen. Habe aber wie gesagt vergessen, warum ich das tun musste.
Alle Änderungen werden quasi erst gebuffert. Die Sperre wäre dann etwas komplexer und würde nur darauf abzielen, das nicht gleichzeigt geschrieben und gelesen wird, mehrfaches/gleichzeitiges Lesen aber erlaubt ist.
Rendering und Logik könnten dann tatsächlich parallel laufen, erst das Ändern der Daten würde das kurz "unterbrechen".
Das mit dem Stack verstehe ich nicht, das hat doch nichts mit dem Thema new/WindowsHeap vs. eigener Speichermanager zu tun, oder?Schrompf hat geschrieben:Wie gesagt: das kannst Du machen, wie Du möchtest. Ich empfände es als unangenehm, nix mehr auf dem Stack anlegen zu können. Da kannst Du auch gleich Java schreiben.
Ich benutze den Speichermanager auch nicht für Alles. Normale Objekte, wie Dateien, Texturen, CriticaelSections, usw., die ich nicht in Massen verwalte, werden normal mit new erstellt und delete gelöscht.
Momentan werden da zwar auch noch virtuelle Delete-Funktionen aufgerufen, aber da die dort eigentlich komplett überflüssig sind, werde ich die wohl Stück für Stück ändern und auf virtuelle Destruktoren setzen.
Ich wusste überhaupt nicht, dass man Destruktoren virtuell machen kann, dass habe ich quasi erst durch euch erfahren, daher muss ich dazu erst rumtesten und werde das dann anwenden.
Mein Hauptziel ist eine funktionsfähige flexible Spieleengine. Die Ultima-Daten nutze ich primär zu Testzwecken, da diese recht komplex sind.Schrompf hat geschrieben:Ist doch cool! Willst Du das Spiel mal komplett nachbauen oder soll das ein reiner Viewer bleiben?
Es wäre aber ein Traum, wenn ich es schaffen würde, mit der Engine Ultima 7/Serpent Isle spielen zu können, da diese zu meinen Lieblingsspielen gehören.
Die meisten Daten von Ultima sind eigentlich ziemlich simpel, aber das Quest -und Dialogsystem scheinen komplexer zu sein, bisher habe ich bei denen immer kapituliert.
Es gibt ein Projekt, dass Ultima 7/Serpent Isle "nachprogrammiert" hat und sich damit auch spielen lässt, Exult (http://exult.sourceforge.net/).
Im Prinzip sind dort alle Informationen zu finden, man muss sich nur die Mühe machen, diese zu analysieren und zu verstehen.
Vielleicht wage ich mich nochmal an das Quest -und Dialogsystem, wenn die Engine fertig ist. Das wäre schon geil, mir fallen zahlreiche Verbesserungen ein, die das Spielerlebnis von Ultima extrem verbessern könnten.
Die Originalversion/Exultversion finde ich schon fast unspielbar, alles ruckelig und zuckelig und lahm. Die Originalversion läuft mit 5 FPS. ^^
Falls jemand Ultima 6, Ultima 7 oder Serpent Isle besitzt, könnte ich bei Interesse eine Testversion der Engine hochladen.
Ich glaube die Daten zu Ultima darf ich selber nicht einfach so verteilen, Copyright usw., und ohne Ultima-Daten ist die Engine so interessant wie ein Gemälde von einem Punkt.
- Krishty
- Establishment
- Beiträge: 8316
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: C++ - Organisation/Verwaltung von millionen Objekten
Am Rande: Doch, x86 hat strenge Garantien mit einigen Einschränkungen (z.B. bzgl. Alignment), sogar strengere als die meisten anderen Architekturen (Alle Integer-Typen mit vernünftiger Ausrichtung lesen ist sogar ohne lock-Präfix atomar!). Allerdings muss man das auch dem Compiler mitteilen, indem man std::atomic nutzt.Schrompf hat geschrieben:Und seltener, aber ebenso möglich, sind direkte Lesefehler: Der Lese-Thread liest die Zahl und bekommt einen Wert, der völliger Quatsch ist. Also z.B. war ein Int vorher bei 0x0000ffe7 und würde danach 0x00010013 haben. x86 (und auch sonst niemand, glaube ich) garantiert nicht, dass die 4Byte des Integers atomar gelesen werden. Du könntest also, wenn der eine Core liest, während der andere schreibt, auch theoretisch ne 0x0001ffe7 lesen, also zwei Byte des alten Wertes und zwei Byte des neuen Wertes. Und das würde man selbst beim simplen Rendering sehen, wenn das Sprite plötzlich einen Meter im Off wäre.
Falls das wirklich geht, würde ich es definitiv hinter einem Makro verstecken und gut dokumentieren. Ich wüsste nicht, was virtual __forceinline bedeuten soll, denn das eine schließt das andere aus.Goderion hat geschrieben:Wegen dem sinnlosen benutzen vom vtable, was tut der Compiler da nur? wenn ich AddRef und Release mit __forceinline versehe, dann wird in der Release-Version der vtable nicht mehr genutzt.
Re: C++ - Organisation/Verwaltung von millionen Objekten
Ich setze nicht bei der Deklaration von virtual AddRef und virtual Release in InterfaceObject das __forceinline, sondern bei der Definition von AddRef und Release in ClassObject.Krishty hat geschrieben:Falls das wirklich geht, würde ich es definitiv hinter einem Makro verstecken und gut dokumentieren. Ich wüsste nicht, was virtual __forceinline bedeuten soll, denn das eine schließt das andere aus.
EDIT: Das mit dem Makro ist eine gute Idee, sonst frage ich mich in einem Jahr, warum ich das gemacht habe. :?
Code: Alles auswählen
// Erst das Definieren der Funktionen AddRef, Release, usw. mit __forceinline sorgen dafür, dass diese in der Release-Version nicht mehr über den VTable aufgerufen werden. Compilerbug?
#define NO_VTABLE __forceinline
- Krishty
- Establishment
- Beiträge: 8316
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: C++ - Organisation/Verwaltung von millionen Objekten
Mal eine ganz dumme Frage: Wenn du die Funktionen in einer abgeleiteten Klasse mit __forceinline, überschreibst, und dann die überschriebene Version durch die Basisschnittstelle aufrufst, ruft der Compiler dann auch mit Optimierungen die korrekte Version (die überschriebene) auf? Das klingt mir sehr anfällig für Compiler-Bugs ...
Re: C++ - Organisation/Verwaltung von millionen Objekten
Jetzt steig ich langsam nicht mehr durch ... :?Krishty hat geschrieben:Mal eine ganz dumme Frage: Wenn du die Funktionen in einer abgeleiteten Klasse mit __forceinline, überschreibst, und dann die überschriebene Version durch die Basisschnittstelle aufrufst, ruft der Compiler dann auch mit Optimierungen die korrekte Version (die überschriebene) auf? Das klingt mir sehr anfällig für Compiler-Bugs ...
Die ersten beiden Tests mit deaktivierten Kontrollfunktionen scheinen gleich zu sein, die Tests mit aktivierten Kontrollfunktionen sind aber unterschiedlich.
Ich denke ich werde den "Weltobjekten" eine neue Basis-Klasse spendieren, wo es kein virtual AddRef und virtual Release mehr geben wird, benötige ich an den Stellen eh nicht, damit sollte das Problem beseitigt sein.
Ich habe für Interessierte/Neugierige mal den Assembler-Code für alle 4 Tests hier per Spoiler eingefügt.
Assembler bei deaktivierten Kontrollfunktionen und ohne __forceinline:
Ich werde vorerst __forceinline entfernen, bzw. das Makro leer definieren.
- Krishty
- Establishment
- Beiträge: 8316
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: C++ - Organisation/Verwaltung von millionen Objekten
Entschuldige, was sind Kontrollfunktionen?
Re: C++ - Organisation/Verwaltung von millionen Objekten
Diesen Punkt von Schrompf und dir möchte ich nocheinmal hervorheben und ergänzen. Ich habe bei mir hervorragende Erfahrungen mit Multibuffern gemacht, da man sich außer beim Buffer-Swap um nichts kümmern muss und zu jeder Zeit einen konsistenten Zustand abgreifen kann.Goderion hat geschrieben:Mmmh... ich glaube ich habe da eine grobe Idee. Ich könnte ein System entwickeln, dass das von dir erwähnte verzögerte löschen/verändern der Daten realisiert.Schrompf hat geschrieben:Joa, aber wie gesagt: verzögertes Erstellen/Löschen musste ich selbst in meinen Single-Threaded-Spielen schon einbauen. Habe aber wie gesagt vergessen, warum ich das tun musste.
Alle Änderungen werden quasi erst gebuffert. Die Sperre wäre dann etwas komplexer und würde nur darauf abzielen, das nicht gleichzeigt geschrieben und gelesen wird, mehrfaches/gleichzeitiges Lesen aber erlaubt ist.
Rendering und Logik könnten dann tatsächlich parallel laufen, erst das Ändern der Daten würde das kurz "unterbrechen".
Du kannst im Prinzip alle relevanten Objekte doppelt- (oder dreifach-) gepuffert erzeugen. Banales Beispiel: std::vector mit Objekten (oder Objektzeigern). Die Logik/Physik läuft auf dem Back-, das Rendering auf dem Frontbuffer. Ist die Logik/Physik mit ihrem Frame fertig, müssen lediglich die beiden Vektoren "geswappt" werden und auch nur dieser Schritt muss z.B. per Mutex gesichert sein (im einfachsten Fall ein Zeigerswap auf den Front- und Backbuffer). Bei mir sind die Frequenzen (z.B. Simulation 500Hz, Rendering 60 Hz) sehr unterschiedlich, hier lohnt sich mindestens ein Triple-Buffer. Die Simulation aktualisiert dann munter mit 500 Hz zwischen Back- und Middle-Buffer und ab und an tauscht die Grafik die Zeiger von Front- und Middle. Falls Du bei deiner Physik auf vergangene Zustände zugreifen und dies komplett über den Buffer abdecken möchtest, wird das Ganze zum "Quadruple"-Buffer. Klingt dramatischer als es ist, hier wird das erste mal ein Kopieren der Zustände benötigt, um einen korrekten vergangenen Zustand zu bekommen (bei einem Swap würde ich einen Frame überspringen). Am Ende sieht das Ganze etwa so aus:
Buffer[0] Simulation(t)
-> Kopie (Simulationsthread)
Buffer[1] Simulation(t-1)
-> Swap (Simulationsthread, geschützt)
Buffer[2] "Austauschbuffer" (t-2)
-> Swap (Renderingthread, geschützt)
Buffer[3] Rendering(t-3)
Das (t-3) sollte auch nicht stören, da es sich auf die Simulationsfrequenz bezieht und die Grafikaktualisierung in diesem Fall deutlich langsamer passiert.
Re: C++ - Organisation/Verwaltung von millionen Objekten
Mit Kontrollfunktionen meine ich Funktionen die diverse Dinge überprüfen.Krishty hat geschrieben:Entschuldige, was sind Kontrollfunktionen?
Jedes Objekt hat z.B. einen Zeiger auf sich selbst. Im ersten Konstruktor wird dieser gesetzt und im letzten Destruktor "genullt". Die Prüfung ist natürlich nicht narrensicher, aber kostet so gut wie nix und die Wahrscheinlichkeit "falsche" Objekte zu erkennen, ist damit sehr hoch.
Die ganze Funktionalität (an/aus) solcher "Kontrollfunktionen" lässt sich innerhalb der gesamten Projektgruppe über ein Define steuern.
Die Idee klingt gut, aber ich weiß nicht, wie ich das in meiner Situation anwenden soll.smurfer hat geschrieben:Diesen Punkt von Schrompf und dir möchte ich nocheinmal hervorheben und ergänzen. Ich habe bei mir hervorragende Erfahrungen mit Multibuffern gemacht, da man sich außer beim Buffer-Swap um nichts kümmern muss und zu jeder Zeit einen konsistenten Zustand abgreifen kann.
Du kannst im Prinzip alle relevanten Objekte doppelt- (oder dreifach-) gepuffert erzeugen. Banales Beispiel: std::vector mit Objekten (oder Objektzeigern). Die Logik/Physik läuft auf dem Back-, das Rendering auf dem Frontbuffer. Ist die Logik/Physik mit ihrem Frame fertig, müssen lediglich die beiden Vektoren "geswappt" werden und auch nur dieser Schritt muss z.B. per Mutex gesichert sein (im einfachsten Fall ein Zeigerswap auf den Front- und Backbuffer). Bei mir sind die Frequenzen (z.B. Simulation 500Hz, Rendering 60 Hz) sehr unterschiedlich, hier lohnt sich mindestens ein Triple-Buffer. Die Simulation aktualisiert dann munter mit 500 Hz zwischen Back- und Middle-Buffer und ab und an tauscht die Grafik die Zeiger von Front- und Middle. Falls Du bei deiner Physik auf vergangene Zustände zugreifen und dies komplett über den Buffer abdecken möchtest, wird das Ganze zum "Quadruple"-Buffer. Klingt dramatischer als es ist, hier wird das erste mal ein Kopieren der Zustände benötigt, um einen korrekten vergangenen Zustand zu bekommen (bei einem Swap würde ich einen Frame überspringen). Am Ende sieht das Ganze etwa so aus:
Buffer[0] Simulation(t)
-> Kopie (Simulationsthread)
Buffer[1] Simulation(t-1)
-> Swap (Simulationsthread, geschützt)
Buffer[2] "Austauschbuffer" (t-2)
-> Swap (Renderingthread, geschützt)
Buffer[3] Rendering(t-3)
Das (t-3) sollte auch nicht stören, da es sich auf die Simulationsfrequenz bezieht und die Grafikaktualisierung in diesem Fall deutlich langsamer passiert.
In einem Level/Karte existieren ca. 250.000 Objekte. Jedes Objekt selber kann je nachdem was es darstellt, 10 bis 100 weitere Objekte/Definitionen besitzen. Handelt es sich dabei um einen Container (Beutel, Rucksack, Truhe, usw.) können es weit über mehrere tausend Objekte sein.
Die Objekte werden über drei Wege im Level gespeichert, wenn sie sich direkt auf der Karte/Level befinden und nicht in einem Container:
1. Octree für Kollisionsberechnungen
2. Quadtree für das Rendering
3. Pro Feld/Zelle, die ein Objekt überdeckt (Z wird hier ignoriert), für das Pathfinding
Ein Objekt existiert somit im Octree, im Quadtree und wenn es z.B. 4 Felder überdeckt, 4 mal als Listeneintrag.
Im Logikthread brauche ich z.B. für die KI alle Daten, die gesamte Karte mit allen Objekten. Mir fällt keine Idee ein, wie ich die Objekte irgendwie sinnvoll buffern könnte und noch weniger, wie ich dann diesen Buffer synchronisiere.
Mein Ansatz wäre eher der, dass die Logik die Änderungen nicht direkt vornimmt, sondern in so eine Art Kommandoliste packt. Die Kommandos sind sehr simpel, wie z.B. reduziere Mana um 10, Bewege Objekt nach X, usw..
Grob gesagt, alle Berechnungen werden im Logikthread vorgenommen, die nötigen Änderungen in primitiver Form gesammelt und dann in einem Rutsch angewandt.
Die Logik braucht dann z.B. 5 ms zum Berechnen und 1 ms zu Ändern der Daten.
Re: C++ - Organisation/Verwaltung von millionen Objekten
Ja, eine Queue abzuarbeiten ist auch ein guter Ansatz (ob nun einzeln oder ergänzend zum Buffer). Du kannst für die Kommandos eine concurrent_queue wie z.B. https://github.com/ikiller1/moodycamel-ConcurrentQueue verwenden oder einfach die Queue doppelt puffern. Bei mir besitzt beispielsweise jedes (Thread-)Modul eine Kommando-Queue (concurrent_queue). Über ein Engine-globales Kommando-Interface werden Funktionsaufrufe automatisch in die korrekte Queue einsortiert. Das Abarbeiten der Aufträge übernimmt dann der jeweilige Thread.Goderion hat geschrieben: Mein Ansatz wäre eher der, dass die Logik die Änderungen nicht direkt vornimmt, sondern in so eine Art Kommandoliste packt. Die Kommandos sind sehr simpel, wie z.B. reduziere Mana um 10, Bewege Objekt nach X, usw..
Grob gesagt, alle Berechnungen werden im Logikthread vorgenommen, die nötigen Änderungen in primitiver Form gesammelt und dann in einem Rutsch angewandt.
Die Logik braucht dann z.B. 5 ms zum Berechnen und 1 ms zu Ändern der Daten.
Re: C++ - Organisation/Verwaltung von millionen Objekten
Vielen Dank für die Antworten/Hinweise!
1. Phase: Lesezugriff auf die Daten anfordern. Objekte lesen, Logik berechnen und die Kommandoliste füllen.
2. Phase: Schreibzugriff auf die Daten anfordern. Kommandoliste auf Daten anwenden.
Demnach wird es nie einen Moment geben, wo mehr als ein Thread gleichzeigt auf die Kommandoliste zugreift. Ich denke das wäre in meinem Fall auch sinnlos.
Die Logik darf nicht schon das nächste "Frame" berechnen, bevor die zuvor erstellte Kommandoliste angewandt wurde.
Die Kommandoliste existiert nur, um die Zeit der "Vollsperrung" der Daten so kurz wie möglich zu halten.
Was meinst Du hier mit Cullen? Ist das nicht nur bei 3D-Anwendungen interessant?
Ich nutze momentan das allerneuste DirectX9! ;)
Ein wechsel zu DX11/DX12/Vulkan/Mantle wäre vermutlich einfach zu realisieren, da ich das Rendersystem strikt vom Rest der Engine getrennt habe.
Ich habe auch nochmal auf diversen Geräten und virtuellen Maschinen new/HeapAlloc/WindowsHeap vs eigener Speichermanager getestet.
Auf meinem Hauptrechner (i7 6700K @4500 - Windows 10) braucht die gesamte Initialisierung mit HeapAlloc ca. 2 Sekunden und der Speichermanager ca. 1,5 Sekunden.
Den größten Unterschied konnte ich auf einem ASUS EeeBook X206HA mit einem Intel Atom x5-Z8350, 4x 1.44GHz feststellen, ca. 17 Sekunden braucht HeapAlloc, der eigene Speichermanager ca. 10 Sekunden.
Die Speicherauslastung war mit HeapAlloc nur 20 bis 30 MB größer, meiner Meinung nach nicht der Rede wert.
Für mich ein etwas ernüchterndes Ergebnis, ich habe zwar noch keine weiteren Tests gemacht, erwarte da aber auch keine großen Unterschiede.
Mein aktuelles Fazit dazu lautet daher: Eigener Speichermanager nett, aber für eine neues Projekt lohnt das vermutlich nicht.
In meinem Fall möchte ich für die Objekte, die massenweise erzeugt und wieder freigegeben werden, so oder so Fabrikfuntionen/Verwalter haben, ohne diese "Fabriken" würde ich den Gebrauch vom Speichermanager vermutlich stark reduzieren oder ihn komplett entfernen.
Ich glaube die concurrent_queue ist für meine Zwecke übertrieben und overpowered. Die Kommando-Liste/Array wäre ja nur ein einfache lineare Aneinanderreihung von Befehlen. Der Logikthread hat meinem Plan zufolge 2 Phasen.smurfer hat geschrieben:Ja, eine Queue abzuarbeiten ist auch ein guter Ansatz (ob nun einzeln oder ergänzend zum Buffer). Du kannst für die Kommandos eine concurrent_queue wie z.B. https://github.com/ikiller1/moodycamel-ConcurrentQueue verwenden oder einfach die Queue doppelt puffern. Bei mir besitzt beispielsweise jedes (Thread-)Modul eine Kommando-Queue (concurrent_queue). Über ein Engine-globales Kommando-Interface werden Funktionsaufrufe automatisch in die korrekte Queue einsortiert. Das Abarbeiten der Aufträge übernimmt dann der jeweilige Thread.
1. Phase: Lesezugriff auf die Daten anfordern. Objekte lesen, Logik berechnen und die Kommandoliste füllen.
2. Phase: Schreibzugriff auf die Daten anfordern. Kommandoliste auf Daten anwenden.
Demnach wird es nie einen Moment geben, wo mehr als ein Thread gleichzeigt auf die Kommandoliste zugreift. Ich denke das wäre in meinem Fall auch sinnlos.
Die Logik darf nicht schon das nächste "Frame" berechnen, bevor die zuvor erstellte Kommandoliste angewandt wurde.
Die Kommandoliste existiert nur, um die Zeit der "Vollsperrung" der Daten so kurz wie möglich zu halten.
Das klingt interessant. Ich bin mir nicht sicher was mit disjunkte Teile gemeint ist, die Weltdaten, bzw. Renderdaten liegen alle pro Karte/Level in einem Quadtree. Spontan würde mir nur die Idee einfallen, dass ich z.B. die zu rendernde Fläche aufteile und ein Thread rendert die linke Seite und der andere die rechte Seite.Schrompf hat geschrieben:Zusätzlich zum "Kann man pauschal so nicht sagen" will ich hinzufügen: Du musst kleiner granularisieren. Das Rendering z.B. ist eine rein lesende Operation auf der Spielwelt. Du könntest also z.B. stressfrei 8 oder wasweißichwieviele Jobs starten, die gleichzeitig über disjunkte Teile der Welt iterieren und die DrawCalls einsammeln. Das Cullen kann man parallelisieren. Mit DX11/DX12/Vulkan/Mantle kann man selbst das Rendern parallelisieren. Daten-Parallele Aufgaben haben immer den Vorteil, dass sie viel besser mit der Hardware skalieren als themen-parallele Aufgaben.
Was meinst Du hier mit Cullen? Ist das nicht nur bei 3D-Anwendungen interessant?
Ich nutze momentan das allerneuste DirectX9! ;)
Ein wechsel zu DX11/DX12/Vulkan/Mantle wäre vermutlich einfach zu realisieren, da ich das Rendersystem strikt vom Rest der Engine getrennt habe.
Ich habe auch nochmal auf diversen Geräten und virtuellen Maschinen new/HeapAlloc/WindowsHeap vs eigener Speichermanager getestet.
Auf meinem Hauptrechner (i7 6700K @4500 - Windows 10) braucht die gesamte Initialisierung mit HeapAlloc ca. 2 Sekunden und der Speichermanager ca. 1,5 Sekunden.
Den größten Unterschied konnte ich auf einem ASUS EeeBook X206HA mit einem Intel Atom x5-Z8350, 4x 1.44GHz feststellen, ca. 17 Sekunden braucht HeapAlloc, der eigene Speichermanager ca. 10 Sekunden.
Die Speicherauslastung war mit HeapAlloc nur 20 bis 30 MB größer, meiner Meinung nach nicht der Rede wert.
Für mich ein etwas ernüchterndes Ergebnis, ich habe zwar noch keine weiteren Tests gemacht, erwarte da aber auch keine großen Unterschiede.
Mein aktuelles Fazit dazu lautet daher: Eigener Speichermanager nett, aber für eine neues Projekt lohnt das vermutlich nicht.
In meinem Fall möchte ich für die Objekte, die massenweise erzeugt und wieder freigegeben werden, so oder so Fabrikfuntionen/Verwalter haben, ohne diese "Fabriken" würde ich den Gebrauch vom Speichermanager vermutlich stark reduzieren oder ihn komplett entfernen.