Speicherzuweisungsmuster, die bei der Spieleentwicklung verwendet werden

20

Ich habe nachgeforscht, wie ich meine eigenen Zuweisungsmethoden erstellen kann (die Dinge wie Speicherpool und Profilerstellung unterstützen), aber während ich meine Nachforschungen fortsetze, habe ich nach Möglichkeiten gesucht, wie dies in der Spieleentwicklung gemacht wurde.

Welche Speicherzuweisungstechnik könnte ich verwenden, und warum ist es eine gute Technik?

chadb
quelle
1
musst du wirklich Es ist nur eines der kompliziertesten Dinge, die ein Team jemals umsetzen kann, wenn es es umsetzen kann.
Ali1S232
4
Es ist ein Interessensgebiet für mich, daher würde ich gerne mehr darüber erfahren und es implementieren
chadb
Ich muss sagen, dass das Thema wirklich interessant ist ... Es gibt Fälle, in denen das viel bedeuten kann, aber bei einem durchschnittlichen PC-Spiel würde ich mir eher Sorgen um das eigentliche Spiel machen ...
rioki
Haben Sie den Quellcode für die moderne Standardbibliothek malloc recherchiert und frei oder neu und löschen? Ich frage, weil es den Anschein hat, dass dies eine sehr nützliche Grundlage für den Vergleich alternativer Allokationsstrategien mit algorithmisch oder praktisch darstellt. Es scheint auch einen echten Einblick in das zu geben, worauf Sie sich einlassen werden.
Louis Langholtz

Antworten:

25

Die Game Engine-Architektur enthält einige Informationen zu diesem Thema. Die Grundlagen sind, dass Sie einige Analysen durchführen müssen, um zu verstehen, was Ihr Speicherbedarf pro Ebene / Frame / etc. sind wie, aber es gibt ein paar Muster, die der Autor mehrfach gesehen hat:

  • Stack-basierte Allokatoren: Diese allokieren einmalig ein großes Speichersegment und ordnen dann Zeiger innerhalb dieses Speicherblocks als Antwort auf Anforderungen von anderen Stellen im Spiel zu. Dies ist nützlich, um Kontextwechsel zu vermeiden, die für die Speicherzuweisung erforderlich sind, und auch, weil Sie Ihre eigenen Techniken verwenden können, um die Kontiguität oder eine bestimmte Ausrichtung für SIMD-Vorgänge zu erzwingen. Einige Engines verwenden auch einen Double-Ended-Stack, bei dem eine Art von Ressource von oben und die andere von unten geladen wird. Vielleicht LSR (Load and Stay Resident, die Art von Dingen, die während des gesamten Spiels benötigt werden) von oben und Daten pro Ebene von unten.
  • Einzelbildspeicher oder doppelt gepufferter Bildspeicher: Speicher für Vorgänge, die innerhalb von ein oder zwei Bildzyklen ausgeführt werden. Dies ist nützlich, da Sie nicht jeden Frame zuweisen / freigeben müssen, sondern einfach die Daten des letzten Frames wegblasen können, indem Sie den Zeiger, mit dem Sie den Speicher verfolgen, auf den Anfang des Blocks zurücksetzen.
  • Objektpools: Ein Speicherblock für viele Objekte gleicher Größe, z. B. Partikel, Feinde und Projektile. Diese sind nützlich, da Sie leicht eine Kontiguität erzielen können, indem Sie das erste nicht verwendete Segment in Ihrem Pool finden. Sie erleichtern auch die Iteration, da jedes Objekt einen bekannten Versatz vom letzten hat.

Das Wichtigste, worauf der Autor achten sollte, ist die Fragmentierung des Gedächtnisses. Dies ist weniger problematisch, wenn Sie z. B. für einen PC entwickeln, auf dem Sie eine Art Paging-Sicherung haben, auf die Sie sich verlassen können. In einem festen Speicherkontext wie einer Konsole besteht jedoch das Risiko, dass nicht genügend Arbeitsspeicher zur Verfügung steht. Wenn Sie versuchen, ein großes Objekt zuzuweisen, da Ihr Speicher so fragmentiert ist, dass nur kleine zusammenhängende Blöcke verfügbar sind. Zu diesem Zweck empfiehlt er, dass ein stapelbasierter Allokator wie oben auch eine Methode zum periodischen Defragmentieren seines Inhalts enthält.

Für weitere Informationen zum eigentlichen Code empfehle ich Christian Gyrlings Artikel "Sind wir nicht mehr im Gedächtnis?". Hier werden Techniken für benutzerdefinierte Zuweiser behandelt, hauptsächlich aus der Perspektive der Analyse von Speichernutzungsmustern. Dies gilt jedoch auch für die Entwicklung einer benutzerdefinierten Lösung für die Speicherverwaltung.


quelle
1

Nach allem, was ich gesehen (aber nicht getan) habe, erbt jedes Spiel die Zuweisungsmechanismen entweder von einem Framework, von einer Game Engine, von der Vorgängerversion (2010 -> 2011), oder es werden eine Reihe neuer, speziell für dieses Framework geschriebener Mechanismen erstellt Struktur (entweder wenn Datenstrukturen wiederverwendbar und von fester Größe sind oder von zahlreichen Typen und variablen Größen).

Außerdem hatten wir andere Zuordnungen für Sounddateien / Komponenten als für Levels und andere Spielobjekte im selben Projekt. In anderen Projekten werden Zuweiser nur für die von dieser Bibliothek verwalteten Komponenten von externen Bibliotheken geerbt.

Die Optimierung hängt wirklich von Ihren Bedürfnissen ab. In der Regel erfolgt die Zuordnung jedoch vor dem Eintritt in die Spielszene, und anschließend wird der Speicher wieder verwendet. Bei einigen Spielen müssen keine benutzerdefinierten Zuweisungen vorgenommen werden. Bei Actionspielen, bei denen Prozessor-, Speicher- und Datenressourcen budgetiert sind, können Sie es sich nicht leisten, bei großen Zuweisungen Verarbeitungszeit zu verlieren, und Sie können keinen Speicher für Fragmentierung und andere Probleme verschwenden.

In Bezug auf Beispiele sollten Sie zunächst einen Blick auf die OGRE 3D- Game-Engine werfen, die einige Optionen zum Konfigurieren von Speicherzuordnungen bietet .

Kojote
quelle
0

Der Fehler, der häufig gemacht wird, besteht darin, Ihre eigenen Allokatoren zu schreiben, damit Sie mehr Kontrolle darüber haben, wie viel Speicher von jedem System verwendet wird, und mehr Einblick in die aktuellen Vorgänge haben. Ein viel besserer Weg, dies zu erreichen, ist die Verwendung eines Speicherprofilers. Es gibt viele Speicher-Profiler, mein Profiler MemPro ist ein Beispiel. Dies ist eine völlig nicht-invasive Methode, um die Speichernutzung im Auge zu behalten, und Sie können sie mithilfe von Callstack-Platzhalterfiltern automatisch in Untersysteme aufteilen. Im Idealfall ist es am besten, die Speicherzuordnung und die Speicherverfolgung vollständig getrennt zu halten, da sie ganz unterschiedliche Anforderungen haben.

Die willkürliche Aufteilung Ihres Speichers in Pools kann oft nachteilig sein, da jeder Pool einen Overhead hat. Sie können am Ende viel mehr Speicher verwenden, als Sie benötigen, ohne es zu bemerken. Um die Verschwendung zu reduzieren, ist es immer besser, alles zusammenzufassen, der Spielraum wird dann vom gesamten System geteilt.

Die einzigen Gründe für die Verwendung benutzerdefinierter Zuordnungen sind die CPU-Leistung (hauptsächlich aus Gründen der Cache-Kohärenz) und die Begrenzung der Fragmentierung. Ein perfektes Beispiel dafür ist ein Partikelsystem. Sie möchten, dass alle Partikel im Speicher zusammenhängend sind, und Sie möchten den Hauptspeicher nicht mit vielen kurzlebigen Zuweisungen überschütten. Ein weiteres gutes Beispiel für das Abschotten ist eine Skriptsprache.

Wenn Sie ein Beispiel für einen Allzweck-Malloc-Ersatz wünschen, können Sie sich meinen VMem-Allokator ansehen . Es wurde in einer Reihe von ausgelieferten AAA-Spielen verwendet. Es verfügt über Techniken, die die Fragmentierung begrenzen und den Speicherbedarf gering halten, was für Konsolenspiele von entscheidender Bedeutung ist. Es ist auch sehr schnell unter hohem Thread-Konflikt. Meine Website enthält umfangreiche Dokumentationen zu diesen Techniken.

Stewart Lynch
quelle