Speicherverwaltung oder seltener schießen Sie sich in den Fuß

Hallo habr In diesem Artikel werde ich versuchen zu erklären, was Speicherverwaltung in Programmen / Anwendungen aus der Sicht eines Anwendungsprogrammierers ist. Dies ist keine vollständige Anleitung oder Anleitung, sondern lediglich eine Übersicht über vorhandene Probleme und einige Lösungsansätze.


Warum ist das notwendig? Ein Programm ist eine Folge von Datenverarbeitungsanweisungen (im allgemeinsten Fall). Diese Daten müssen auf irgendeine Weise gespeichert , geladen , übertragen usw. werden. Alle diese Vorgänge werden nicht sofort ausgeführt, daher wirken sie sich direkt auf die Geschwindigkeit Ihrer endgültigen Anwendung aus. Die Fähigkeit, Daten während des Arbeitsprozesses optimal zu verwalten, ermöglicht es Ihnen, sehr nicht triviale und sehr ressourcenintensive Programme zu erstellen.


Hinweis: Der Großteil des Materials wird mit Beispielen aus Spielen / Spiele-Engines präsentiert (da dieses Thema für mich persönlich interessanter ist). Der größte Teil des Materials kann jedoch zum Schreiben von Servern, Benutzeranwendungen, Grafikpaketen usw. verwendet werden.



Es ist unmöglich, alles im Auge zu behalten. Wenn Sie es jedoch nicht geschafft haben, es zu laden, erhalten Sie Seife


Auf Anhieb


In der Branche kam es vor, dass große AAA-Spielprojekte hauptsächlich auf Engines entwickelt wurden, die mit C ++ geschrieben wurden. Eines der Merkmale dieser Sprache ist die Notwendigkeit einer manuellen Speicherverwaltung. Java / C # usw. Sie verfügen über Garbage Collection (GarbageCollection / GC) - die Fähigkeit, Objekte zu erstellen und dennoch nicht den verwendeten Speicher von Hand freizugeben. Dieser Prozess vereinfacht und beschleunigt die Entwicklung, kann aber auch einige Probleme verursachen: Ein periodisch ausgelöster Garbage Collector kann alle Soft-Echtzeit-Aktionen beenden und dem Spiel unangenehme Einfrierungen hinzufügen.


Ja, in Projekten wie "Minecraft" ist der GC möglicherweise nicht erkennbar Sie stellen im Allgemeinen keine hohen Anforderungen an die Ressourcen des Computers, aber Spiele wie "Red Dead Redemption 2", "God of War" und "Last of Us" arbeiten "fast" auf dem Höhepunkt der Systemleistung und müssen daher nicht nur groß sein die Menge der Ressourcen, aber auch in ihrer kompetenten Verteilung.


Wenn Sie in einer Umgebung mit automatischer Speicherzuweisung und Speicherbereinigung arbeiten, kann es außerdem zu mangelnder Flexibilität bei der Verwaltung von Ressourcen kommen. Es ist kein Geheimnis, dass Java alle Implementierungsdetails und Aspekte seiner Arbeit unter der Haube verbirgt. Am Ausgang haben Sie also nur die installierte Schnittstelle für die Interaktion mit Systemressourcen, aber es reicht möglicherweise nicht aus, um einige Probleme zu lösen. Das Starten eines Algorithmus mit einer nicht konstanten Anzahl von Speicherzuweisungen in jedem Frame (dies kann eine Suche nach Pfaden für KI, Überprüfen der Sichtbarkeit, Animation usw. sein) führt unweigerlich zu einem katastrophalen Leistungsabfall.


Wie Zuordnungen im Code aussehen


Bevor ich die Diskussion fortsetze, möchte ich anhand einiger Beispiele zeigen, wie die Arbeit mit dem Speicher in C / C ++ direkt abläuft. Im Allgemeinen wird die standardmäßige und einfachste Schnittstelle zum Zuweisen von Prozessspeicher durch die folgenden Operationen dargestellt:


//        size  void* malloc(size_t size); //      p void free(void* p); 

Hier können Sie weitere Funktionen hinzufügen, mit denen Sie einen ausgerichteten Speicherplatz zuweisen können:


 // C11  -     , * alignment void* aligned_alloc(size_t size, size_t alignment); // Posix  -       //        address (*address = allocated_mem_p) int posix_memalign(void** address, size_t alignment, size_t size); 

Bitte beachten Sie, dass verschiedene Plattformen möglicherweise unterschiedliche Funktionsstandards unterstützen, die beispielsweise unter macOS und unter win nicht verfügbar sind.


Mit Blick auf die Zukunft sind möglicherweise speziell ausgerichtete Speicherbereiche erforderlich, damit Sie sowohl die Prozessor-Cache-Zeile erreichen als auch Berechnungen mit einem erweiterten Registersatz ( SSE , MMX , AVX usw.) durchführen können.


Ein Beispiel für ein Spielzeugprogramm, das Speicher zuweist, Pufferwerte druckt und diese als vorzeichenbehaftete Ganzzahlen interpretiert:


 /* main.cpp */ #include <cstdio> #include <cstdlib> int main(int argc, char** argv) { const int N = 10; int* buffer = (int*) malloc(sizeof(int) * N); for(int i = 0; i < N; i++) { printf("%i ", buffer[i]); } free(buffer); return 0; } 

Unter macOS 10.14 kann dieses Programm mit den folgenden Befehlen erstellt und ausgeführt werden:


 $ clang++ main.cpp -o main $ ./main 

Hinweis: Im Folgenden möchte ich C ++ - Operationen wie new / delete nicht wirklich behandeln, da sie eher zum direkten Erstellen / Zerstören von Objekten verwendet werden, aber sie verwenden die üblichen Operationen mit Speicher wie malloc / free.


Speicherprobleme


Bei der Arbeit mit dem RAM des Computers treten verschiedene Probleme auf. Alle auf die eine oder andere Weise werden nicht nur durch die Funktionen des Betriebssystems und der Software verursacht, sondern auch durch die Architektur des Bügeleisens, auf dem all diese Dinge funktionieren.


1. Speicherplatz


Leider ist der Speicher physisch begrenzt. Auf der PlayStation 4 sind dies 8 GiB GDDR5, von denen 3,5 GiB das Betriebssystem für seine Bedürfnisse reserviert . Der Austausch von virtuellem Speicher und Seiten hilft nicht viel, da das Austauschen von Seiten auf die Festplatte ein sehr langsamer Vorgang ist (innerhalb von festen N Bildern pro Sekunde, wenn es um Spiele geht).


Erwähnenswert ist auch das begrenzte " Budget " - eine künstliche Begrenzung der verwendeten Speichermenge, die erstellt wird, um die Anwendung auf mehreren Plattformen auszuführen. Wenn Sie ein Spiel für eine mobile Plattform erstellen und nicht nur eines, sondern eine ganze Reihe von Geräten unterstützen möchten, müssen Sie Ihren Appetit einschränken, um einen breiteren Absatzmarkt zu schaffen. Dies kann sowohl durch einfaches Begrenzen des RAM-Verbrauchs als auch durch die Möglichkeit erreicht werden, diese Einschränkung abhängig vom Gadget zu konfigurieren, auf dem das Spiel tatsächlich startet.


2. Fragmentierung


Ein unangenehmer Effekt, der während des Prozesses der mehrfachen Zuordnung von Speicherstücken unterschiedlicher Größe auftritt. Als Ergebnis erhalten Sie einen Adressraum, der in viele separate Teile fragmentiert ist. Das Kombinieren dieser Teile zu einzelnen Blöcken größerer Größe funktioniert nicht, da ein Teil des Speichers belegt ist und wir ihn nicht frei bewegen können.



Fragmentierung am Beispiel sequentieller Zuordnungen und Freigaben von Speicherblöcken


Als Ergebnis: Wir können quantitativ, aber nicht qualitativ genug freien Speicher haben. Und bei der nächsten Anforderung, beispielsweise "Speicherplatz für die Audiospur zuweisen", kann der Zuweiser diese nicht erfüllen, da es einfach kein einzelnes Speicherelement dieser Größe gibt.


3. CPU-Cache



Computerspeicherhierarchie


Der Cache eines modernen Prozessors ist eine Zwischenverbindung, die den Hauptspeicher (RAM) und die Prozessorregister direkt verbindet. Es kam vor, dass der Lese- / Schreibzugriff auf den Speicher eine sehr langsame Operation ist (wenn wir über die Anzahl der zur Ausführung erforderlichen CPU-Taktzyklen sprechen). Daher gibt es eine Cache-Hierarchie (L1, L2, L3 usw.), die es sozusagen "gemäß einer Vorhersage" ermöglicht, Daten aus dem RAM zu laden oder sie langsam in einen langsameren Speicher zu verschieben.


Durch das Platzieren von Objekten desselben Typs in einer Zeile im Speicher können Sie den Verarbeitungsprozess "erheblich" beschleunigen (wenn die Verarbeitung nacheinander erfolgt), da in diesem Fall leichter vorhergesagt werden kann, welche Daten als Nächstes benötigt werden. Und mit "signifikant" sind manchmal Produktivitätssteigerungen gemeint. Die Entwickler der Unity-Engine haben in ihren Berichten bei der GDC wiederholt darüber gesprochen.


4. Multithreading


Das Sicherstellen eines sicheren Zugriffs auf gemeinsam genutzten Speicher in einer Umgebung mit mehreren Threads ist eines der Hauptprobleme, die Sie lösen müssen, wenn Sie Ihre eigene Spiel-Engine / Spiel / jede andere Anwendung erstellen, die mehrere Threads verwendet, um eine höhere Leistung zu erzielen. Moderne Computer sind sehr trivial angeordnet. Wir haben sowohl eine komplexe Cache-Struktur als auch mehrere Taschenrechner-Kerne. All dies kann bei unsachgemäßer Verwendung zu Situationen führen, in denen die gemeinsam genutzten Daten Ihres Prozesses durch mehrere Threads beschädigt werden (wenn sie gleichzeitig versuchen, mit diesen Daten ohne Zugriffskontrolle zu arbeiten). Im einfachsten Fall sieht es so aus:

Ich möchte mich nicht mit dem Thema Multithread-Programmierung befassen, da viele seiner Aspekte sehr weit über den Rahmen des Artikels oder sogar des gesamten Buches hinausgehen.


5. Malloc / frei


Zuordnungs- / Freigabevorgänge werden nicht sofort ausgeführt. Unter modernen Betriebssystemen sind Windows / Linux / MacOS gut implementiert und funktionieren in den meisten Situationen schnell . Dies ist jedoch möglicherweise ein sehr zeitaufwändiger Vorgang. Dies ist nicht nur ein Systemaufruf, sondern es kann je nach Implementierung eine Weile dauern, bis ein geeigneter Speicherplatz (First Fit, Best Fit usw.) gefunden oder ein Platz zum Einfügen und / oder Zusammenführen des freigegebenen Bereichs gefunden wurde.


Darüber hinaus wird der frisch zugewiesene Speicher möglicherweise nicht auf reale physische Seiten abgebildet, was auch beim ersten Zugriff einige Zeit in Anspruch nehmen kann.


Dies sind Implementierungsdetails, aber was ist mit der Anwendbarkeit? Malloc / new haben keine Ahnung, wo, wie oder warum Sie sie angerufen haben. Sie weisen (im schlimmsten Fall) Speicher von 1 KiB und 100 MiB gleichermaßen zu ... gleich schlecht. Die Verwendungsstrategie bleibt direkt entweder dem Programmierer oder demjenigen überlassen, der die Laufzeit Ihres Programms implementiert hat.


6. Speicherbeschädigung


Wie das Wiki sagt , ist dies einer der unvorhersehbarsten Fehler, der nur im Verlauf des Programms auftritt und meistens direkt durch Fehler beim Schreiben dieses Programms verursacht wird. Aber was ist das für ein Problem? Glücklicherweise (oder leider) hängt es nicht mit der Beschädigung Ihres Computers zusammen. Es wird vielmehr eine Situation angezeigt, in der Sie versuchen, mit Speicher zu arbeiten , der Ihnen nicht gehört . Ich werde jetzt erklären:


  1. Dies kann ein Versuch sein, in einen Teil des nicht zugewiesenen Speichers zu lesen / schreiben.
  2. Über die Grenzen des für Sie bereitgestellten Speicherblocks hinausgehen. Dieses Problem ist eine Art Sonderfall des Problems (1), aber es ist schlimmer, weil das System Ihnen mitteilt, dass Sie die Grenzen nur überschritten haben, wenn Sie die für Sie angezeigte Seite verlassen. Das heißt, dieses Problem ist möglicherweise sehr schwer zu erkennen, da das Betriebssystem nur dann reagieren kann, wenn Sie die Grenzen der angezeigten virtuellen Seiten belassen. Sie können den Prozessspeicher verderben und einen sehr seltsamen Fehler an der Stelle erhalten, von der er überhaupt nicht erwartet wurde.
  3. Freigeben eines bereits freigegebenen (klingt seltsam) oder noch nicht zugewiesenen Speichers
  4. usw.

In C / C ++, wo es Zeigerarithmetik gibt, werden Sie ein- oder zweimal darauf stoßen. In Java Runtime muss man jedoch ziemlich stark schwitzen, um diese Art von Fehler zu bekommen (ich habe es nicht selbst versucht, aber ich denke, dass dies möglich ist, sonst wäre das Leben zu einfach).


7. Speicherlecks


Es ist ein Sonderfall eines allgemeineren Problems, das in vielen Programmiersprachen auftritt. Die Standard-C / C ++ - Bibliothek bietet Zugriff auf Betriebssystemressourcen. Dies können Dateien, Sockets, Speicher usw. sein. Nach der Verwendung muss die Ressource korrekt geschlossen sein und
Die von ihm besetzte Erinnerung sollte befreit werden. Und wenn wir speziell über die Freigabe von Speicher sprechen - akkumulierte Lecks infolge des Programms können zu einem Fehler "Nicht genügend Speicher" führen, wenn das Betriebssystem die nächste Zuweisungsanforderung nicht erfüllen kann. Oft vergisst der Entwickler einfach, den verwendeten Speicher aus dem einen oder anderen Grund freizugeben.


Hier lohnt es sich, etwas über das korrekte Schließen und Freigeben von Ressourcen auf der GPU hinzuzufügen, da die frühen Treiber die Arbeit mit der Grafikkarte nicht wieder aufnehmen konnten, wenn die vorherige Sitzung nicht korrekt abgeschlossen wurde. Nur ein Neustart des Systems könnte dieses Problem lösen, was sehr zweifelhaft ist - den Benutzer zu zwingen, das System nach dem Ausführen Ihrer Anwendung neu zu starten.


8. Baumelnder Zeiger


Ein baumelnder Zeiger ist eine Fachsprache, die eine Situation beschreibt, in der ein Zeiger auf einen ungültigen Wert verweist. Eine ähnliche Situation kann leicht auftreten, wenn klassische C-Zeiger in einem C / C ++ - Programm verwendet werden. Angenommen, Sie haben Speicher zugewiesen, die Adresse im p-Zeiger gespeichert und dann den Speicher freigegeben (siehe Codebeispiel):


 //   void* p = malloc(size); // ...  -    //   free(p); //    p? // *p == ? 

Der Zeiger speichert einen Wert, den wir als Adresse des Speicherblocks interpretieren können. Es ist also passiert, dass wir nicht sagen können, ob dieser Speicherblock gültig ist oder nicht. Nur ein Programmierer, der auf bestimmten Vereinbarungen basiert, kann mit einem Zeiger arbeiten. Beginnend mit C ++ 11 wurden eine Reihe zusätzlicher „intelligenter Zeiger“ in die Standardbibliothek aufgenommen, die es dem Programmierer auf irgendeine Weise ermöglichen, die Ressourcensteuerung zu schwächen, indem zusätzliche Metainformationen in sich selbst verwendet werden (dazu später mehr).


Als Teillösung können Sie den speziellen Wert des Zeigers verwenden, der uns signalisiert, dass sich an dieser Adresse nichts befindet. In C wird das NULL-Makro als Wert für diesen Wert verwendet, und in C ++ wird das Schlüsselwort nullptr verwendet. Die Lösung ist teilweise, weil:


  1. Der Zeigerwert muss manuell eingestellt werden, damit der Programmierer dies einfach vergessen kann.
  2. nullptr oder nur 0x0 ist in der vom Zeiger akzeptierten Wertemenge enthalten, was nicht gut ist, wenn der spezielle Zustand eines Objekts durch seinen normalen Zustand ausgedrückt wird. Dies ist eine Art Vermächtnis, und nach Vereinbarung weist Ihnen das Betriebssystem keinen Speicher zu, dessen Adresse mit 0x0 beginnt.

Beispielcode mit null:


 //  -  p free(p); p = nullptr; //   p == nullptr   ,        

Sie können diesen Prozess bis zu einem gewissen Grad automatisieren:


 void _free(void* &p) { free(p); p = nullptr; } //  -  p _free(p); //   p == nullptr,     //    

9. Art des Speichers


RAM ist ein gewöhnlicher Allzweck-Direktzugriffsspeicher, auf den über den zentralen Bus alle Kerne Ihres Prozessors und Ihrer Peripheriegeräte zugreifen können. Das Volumen variiert, aber meistens handelt es sich um N Gigabyte, wobei N 1,2,4,8,16 usw. ist. Ruft malloc / free auf, um den gewünschten Speicherblock direkt im RAM des Computers zu platzieren.


VRAM (Videospeicher) - Videospeicher, der mit der Grafikkarte / dem Videobeschleuniger Ihres PCs geliefert wird. Es ist in der Regel kleiner als RAM (ca. 1,2,4 GiB), hat aber eine hohe Geschwindigkeit. Die Verteilung dieses Speichertyps wird vom Grafikkartentreiber übernommen, und meistens haben Sie keinen direkten Zugriff darauf.


Auf der PlayStation 4 gibt es keine solche Trennung, und der gesamte Arbeitsspeicher wird auf GDDR5 durch einzelne 8 Gigabyte dargestellt. Daher sind alle Daten sowohl für den Prozessor als auch für den Videobeschleuniger in der Nähe.


Ein gutes Ressourcenmanagement in der Spiel-Engine umfasst eine kompetente Speicherzuweisung sowohl im Haupt-RAM als auch auf der VRAM-Seite. Hier kann es zu Duplikaten kommen, wenn dieselben Daten vorhanden sind oder wenn Daten übermäßig vom RAM zum VRAM übertragen werden und umgekehrt.


Zur Veranschaulichung aller angesprochenen Probleme : Sie können die Aspekte der Gerätecomputer am Beispiel der PlayStation 4-Architektur betrachten (Abb.). Hier ist der Zentralprozessor, 8 Kerne, Caches auf L1- und L2-Ebene, Datenbusse, RAM, Grafikbeschleuniger usw. Eine vollständige und detaillierte Beschreibung finden Sie in Jason Gregorys "Game Engine Architecture" .



PlayStation 4-Architektur


Allgemeine Ansätze


Es gibt keine universelle Lösung. Es gibt jedoch einige Punkte, auf die Sie sich konzentrieren sollten, wenn Sie die manuelle Zuordnung und Speicherverwaltung in Ihrer Anwendung implementieren möchten. Dies umfasst Container und spezialisierte Zuweiser, Speicherzuweisungsstrategien, System- / Spieldesign, Ressourcenmanager und mehr.


Arten von Allokatoren


Die Verwendung spezieller Speicherzuordnungen basiert auf der folgenden Idee: Sie wissen, welche Größe, zu welchen Arbeitszeiten und an welcher Stelle Sie Speicher benötigen. Daher können Sie den erforderlichen Speicher zuweisen, ihn irgendwie strukturieren und verwenden / wiederverwenden. Dies ist die allgemeine Idee / das Konzept der Verwendung spezieller Allokatoren. Was sie sind (natürlich nicht alle), kann weiter gesehen werden:


  1. Linearer Allokator
    Stellt einen zusammenhängenden Adressraumpuffer dar. Im Laufe der Arbeit können Sie Speicherabschnitte beliebiger Größe zuordnen (so dass sie in einen Puffer passen). Sie können den gesamten zugewiesenen Speicher jedoch nur einmal freigeben. Das heißt, ein beliebiger Speicherplatz kann nicht freigegeben werden - er bleibt so, als wäre er belegt, bis der gesamte Puffer als sauber markiert ist. Diese Konstruktion ermöglicht die Zuweisung und Freigabe von O (1), was unter allen Bedingungen eine Geschwindigkeitsgarantie bietet.

    Typischer Anwendungsfall: Während der Aktualisierung des Prozessstatus (jedes Frames im Spiel) können Sie mit LinearAllocator tmp-Puffer für alle technischen Anforderungen zuweisen: Eingabeverarbeitung, Arbeiten mit Zeichenfolgen, Analysieren von ConsoleManager-Befehlen im Debug-Modus usw.


  2. Stapelverteiler
    Modifikation eines linearen Allokators. Ermöglicht das Freigeben von Speicher in umgekehrter Reihenfolge der Zuordnung, dh verhält sich wie ein regulärer Stapel nach dem LIFO-Prinzip. Es kann sehr nützlich sein, um geladene mathematische Berechnungen (Hierarchie von Transformationen) durchzuführen, um die Arbeit des Skriptsubsystems zu implementieren, für alle Berechnungen, bei denen die angegebene Reihenfolge der Speicherfreigabe im Voraus bekannt ist.

    Die Einfachheit des Designs bietet eine O (1) -Speicherzuordnung und Freigabegeschwindigkeit.


  3. Pool Allokator
    Ermöglicht das Zuweisen von Speicherblöcken derselben Größe. Es kann als Puffer eines kontinuierlichen Adressraums implementiert werden, der in Blöcke einer vorbestimmten Größe unterteilt ist. Diese Blöcke können eine verknüpfte Liste bilden. Und wir wissen immer, welchen Block wir bei der nächsten Zuordnung geben sollen. Diese Metainformationen können in den Blöcken selbst gespeichert werden, wodurch die minimale Blockgröße (sizeof (void *)) eingeschränkt wird. In Wirklichkeit ist dies nicht kritisch.

    Da alle Blöcke gleich groß sind, spielt es für uns keine Rolle, welcher Block zurückgegeben werden soll. Daher können alle Zuordnungs- / Freigabevorgänge in O (1) ausgeführt werden.


  4. Rahmenverteiler
    Linearer Allokator, jedoch nur mit Bezug auf den aktuellen Frame - Ermöglicht die Zuweisung von MPP-Speicher und die automatische Freigabe aller Elemente beim Ändern des Frames. Es sollte separat herausgegriffen werden, da dies eine globale und einzigartige Einheit im Rahmen des Laufzeitspiels ist und daher eine sehr beeindruckende Größe haben kann, beispielsweise ein paar Dutzend MiB, die beim Laden und Verarbeiten von Ressourcen sehr nützlich sein wird.


  5. Doppelrahmen-Allokator
    Es ist ein Doppelrahmen-Allokator, aber mit einigen Funktionen. Sie können Speicher im aktuellen Frame zuweisen und sowohl im aktuellen als auch im nächsten Frame verwenden. Das heißt, der Speicher, den Sie in Frame N zugewiesen haben, wird erst nach N + 1 Frame freigegeben. Dies wird durch Umschalten des aktiven Rahmens auf Hervorheben am Ende jedes Rahmens realisiert.

    Diese Art von Allokator unterwirft jedoch wie die vorherige eine Reihe von Einschränkungen für die Lebensdauer von Objekten, die in dem ihm zugewiesenen Speicher erstellt wurden. Daher sollten Sie sich bewusst sein, dass die Daten am Ende des Frames einfach ungültig werden und ein wiederholter Zugriff auf sie schwerwiegende Probleme verursachen kann.


  6. Statischer Allokator
    Diese Art von Allokator reserviert Speicher aus einem Puffer, der beispielsweise beim Programmstart erhalten oder in einem Funktionsrahmen auf dem Stapel erfasst wurde. Nach Typ kann es sich um einen beliebigen beliebigen Allokator handeln: linear, Pool, Stack. Warum heißt es statisch ? Die Größe des erfassten Speicherpuffers sollte in der Phase der Kompilierung des Programms bekannt sein. Dies stellt eine erhebliche Einschränkung dar: Die für diesen Allokator verfügbare Speichermenge kann während des Betriebs nicht geändert werden. Aber was sind die Vorteile? Der verwendete Puffer wird automatisch erfasst und dann freigegeben (entweder nach Abschluss der Arbeiten oder nach Verlassen der Funktion). Dadurch wird der Heap nicht geladen, Sie werden vor Fragmentierung bewahrt und können schnell Speicherplatz zuweisen.
    Sie können sich das Codebeispiel mit diesem Allokator ansehen, wenn Sie die Zeichenfolge in Teilzeichenfolgen aufteilen und etwas damit tun müssen:

    Es kann auch angemerkt werden, dass die Verwendung von Speicher aus dem Stapel theoretisch viel effizienter ist, weil Stapeln Sie den Frame der aktuellen Funktion mit hoher Wahrscheinlichkeit bereits im Prozessor-Cache.



Alle diese Allokatoren lösen irgendwie die Probleme mit der Fragmentierung, mit einem Mangel an Speicher, mit der Geschwindigkeit des Empfangens und Freigebens von Blöcken der erforderlichen Größe, mit der Lebensdauer von Objekten und dem Speicher, den sie belegen.


Es sollte auch beachtet werden, dass Sie mit dem richtigen Ansatz für das Schnittstellendesign eine Art Hierarchie von Zuweisern erstellen können, wenn beispielsweise: Pool Speicher aus Frame-Zuweisung zuweist und Frame-Zuweisung wiederum Speicher aus linearer Zuweisung zuweist. Eine ähnliche Struktur kann weitergeführt werden und sich an Ihre Aufgaben und Bedürfnisse anpassen.



Ich sehe eine ähnliche Schnittstelle zum Erstellen von Hierarchien wie folgt:


 class IAllocator { public: virtual void* alloc(size_t size) = 0; virtual void* alloc(size_t size, size_t alignment) = 0; virtual void free (void* &p) = 0; } 

malloc/free , . , , . / , .



Smart pointer — C++ ++11 ( boost, ). -, , - , . .


? :


  1. (/)

:


  1. Unique pointer
    1 ( ).
    unique pointer , . , .. 1 / .
    uniquePtr1 uniquePtr2, uniquePtr1 , . 1 .


  2. Shared pointer
    (reference counting). , , . , , , .

    . -, , . . -, - .


  3. Weak pointer
    . , . ? shared pointer. , shared pointer , . , shared pointer weak pointer. , (shared) , weak pointer shared pointer. — weak pointer , , , .

    shared, weak pointer meta-data . - , .. , O(N) overhead , N — - . , . , . .



: . , shared pointer, , ( ) - - - . . meta-info , , . Ein Beispiel:


 /*     */ /*   ,  shared pointer */ Array<TSharedPtr<Object>> objects; objects.add(newShared<Object>(...)); ... objects.add(newShared<Object>(...)); 

 /*      (   meta-info    ) */ Array<Object> objects; objects.emplace(...); ... objects.emplace(...); 

. . Darüber weiter.


Unique id


, . (id/identificator), , , -. :



  1. , id. , , , id.

  2. , ( , )

  3. id , , id.

  4. . , id, .

: id, , id, .


id , (Vulkan, OpenGL), (Godot, CryEngine). EntityID CryEngine .


, id : . , ( ), , .


 /*    */ class ID { uint32 index; uint32 generation; } 

 /*  - /  */ class ObjectManager { public: ID create(...); void destroy(ID); void update(ID id, ...); private: Array<uint32> generations; Array<Objects> objects; } 

ID , ID . :


 generation = generations[id.index]; if (generation == id.generation) then /*    */ else /*  ,     */ 

id generation 1 id ids.


Container


C++ , . std, , . :


  • Linked list —
  • Array — /
  • Queue —
  • Stack —
  • Map —
  • Set —

? memory corruption. / , , , , .



, , . , , / .



, , . , ( ) . , malloc/free , , .


? , (/ ), , , . , , , .



ryEngine Sandbox:


, Unreal, Unity, CryEngine ., , . , , , — , .


Pre-allocating


, / .


: malloc/free . , "run out of memory", . . , (, , .).


. . , - . , malloc/free, : , , .



. : , , , .. .


: , , , . open-source , , . , , — malloc/free.



GDC CD Project Red , , "The Witcher: Blood and Wine" () . , , , , .


Naughty Dog , "Uncharted 4: A Thief's End" , (, ) .


Fazit


, , , . , . / , , - .. , (, ).



Source: https://habr.com/ru/post/de473294/


All Articles