
Fast alle nicht trivialen Programme weisen dynamischen Speicher zu und verwenden ihn. Die korrekte Ausführung wird immer wichtiger, da Programme immer komplexer werden und Fehler noch teurer werden.
Typische Probleme sind:
- Speicherlecks (nicht verbrauchten Speicher freigeben)
- Doppelfreigabe (Speicherfreigabe mehr als einmal)
- Verwendung nach Freigabe (Verwendung eines Zeigers auf einen zuvor freigegebenen Speicher)
Die Aufgabe besteht darin, die Zeiger zu verfolgen, die für die Freigabe des Speichers verantwortlich sind (d. H. Diejenigen, denen der Speicher gehört), und Zeiger zu unterscheiden, die einfach auf ein Stück Speicher zeigen, zu steuern, wo sie sich befinden und welche von ihnen aktiv sind (im Umfang).
Typische Lösungen sind wie folgt:
- Garbage Collection (GC) - GC besitzt Speicherblöcke und durchsucht diese regelmäßig nach Zeigern auf diese Blöcke. Wenn keine Zeiger gefunden werden, wird Speicher freigegeben. Dieses Schema ist zuverlässig und wird in Sprachen wie Go und Java verwendet. Aber GC hat die Tendenz, viel mehr Speicher als nötig zu verwenden, hat Pausen und verlangsamt den Code aufgrund des Umpackens (orig.inserted Write Gates).
- Referenzzählung (RC) - Ein RC-Objekt besitzt Speicher und speichert einen Zeigerzähler für sich. Wenn dieser Zähler auf Null abfällt, wird Speicher freigegeben. Es ist auch ein zuverlässiger Mechanismus und wird in Sprachen wie C ++ und ObjectiveC akzeptiert. RC ist speichereffizient und benötigt zusätzlich nur Platz unter dem Zähler. Die negativen Aspekte von RC sind der Aufwand für die Wartung des Zählers, die Einbettung eines Ausnahmebehandlers zur Gewährleistung seiner Reduzierung und die Blockierung, die für Objekte erforderlich ist, die von Programmabläufen gemeinsam genutzt werden. Um die Leistung zu verbessern, haben Programmierer manchmal betrogen, indem sie vorübergehend auf ein RC-Objekt verwiesen haben, das den Zähler umging, wodurch das Risiko einer falschen Ausführung entstand.
- Manuelle Steuerung - Die manuelle Speicherverwaltung ist Sysalny malloc und kostenlos. Es ist schnell und effizient in Bezug auf die Speichernutzung, aber die Sprache hilft nicht, alles richtig zu machen, da sie sich vollständig auf die Erfahrung und den Eifer des Programmierers verlässt. Ich benutze malloc und free seit 35 Jahren und mache mit Hilfe einer bitteren und endlosen Erfahrung selten Fehler. Aber auf diese Art und Weise kann sich die Programmiertechnologie nicht verlassen, und ich habe "selten" und nicht "nie" gesagt.
Die Lösungen 2 und 3 sind bis zu dem einen oder anderen Grad auf das Vertrauen in den Programmierer angewiesen, um alles richtig zu machen. Systeme, die auf Glauben basieren, lassen sich nicht gut skalieren, und Speicherverwaltungsfehler sind nachweislich sehr schwer zu überprüfen (so schlecht, dass einige Codierungsstandards die Verwendung von dynamischem Speicher verbieten).
Es gibt aber auch einen vierten Weg - Ownership and Borrowing, OB. Es ist speichereffizient, so schnell wie der manuelle Betrieb und wird automatisch überprüft. Die Methode wurde kürzlich von der Programmiersprache Rust populär gemacht. Es hat auch seine Nachteile, insbesondere die Notwendigkeit, die Planung von Algorithmen und Datenstrukturen zu überdenken.
Sie können sich mit negativen Aspekten befassen, und der Rest dieses Artikels enthält eine schematische Beschreibung der Funktionsweise des OB-Systems und der Vorschläge, es in die D-Sprache zu schreiben. Ich hielt dies zunächst für unmöglich, fand aber nach einiger Zeit des Nachdenkens einen Weg. Es ist ähnlich wie bei der funktionalen Programmierung - mit transitiver Unveränderlichkeit und "reinen" Funktionen.
Besitz
Die Entscheidung, wem das Objekt im Speicher gehört, ist lächerlich einfach - es gibt einen einzelnen Zeiger auf das Objekt und es ist der Eigentümer. Er ist auch für die Freigabe des Gedächtnisses verantwortlich, wonach es ungültig wird. Aufgrund der Tatsache, dass der Zeiger auf das Objekt im Speicher der Eigentümer ist, gibt es keine anderen Zeiger in dieser Datenstruktur, und daher bildet die Datenstruktur einen Baum.
Die zweite Konsequenz ist, dass Zeiger die Semantik des Verschiebens verwenden, anstatt zu kopieren:
T* f(); void g(T*); T* p = f(); T* q = p;
Das Entfernen eines Zeigers aus einer Datenstruktur ist verboten:
struct S { T* p; } S* f(); S* s = f(); T* q = sp;
Warum nicht einfach sp als ungültig markieren? Das Problem ist, dass dies das Festlegen des Labels zur Laufzeit erfordert, aber in der Kompilierungsphase behoben werden sollte, da dies einfach als Kompilierungsfehler betrachtet wird.
Das Verlassen des eigenen Zeigers außerhalb des Gültigkeitsbereichs ist ebenfalls ein Fehler:
void h() { T* p = f(); }
Sie müssen den Zeigerwert anders verschieben:
void g(T*); void h() { T* p = f(); g(p);
Dies löst die Speicherverlustprobleme und die Verwendung nach dem Freigeben (Hinweis: Ersetzen Sie aus Gründen der Übersichtlichkeit f () durch malloc () und g () durch free ().)
All dies kann in der Kompilierungsphase mithilfe der
DFA- Technik
(Data Flow Analysis) überprüft werden, ähnlich wie sie zum
Entfernen gängiger Unterausdrücke verwendet wird . DFA kann jegliches Rattengewirr von möglicherweise auftretenden Programmübergängen abwickeln.
Ausleihen
Das oben beschriebene Tenure-System ist zuverlässig, aber zu restriktiv.
Bedenken Sie:
struct S { void car(); void bar(); } struct S* f(); S* s = f(); s.car();
Damit dies funktioniert, muss s.car () eine Möglichkeit haben, den Zeiger beim Beenden wieder zu erhalten.
So funktioniert das Ausleihen. s.car () nimmt eine Kopie von s für die Dauer von s.car (). s ist zur Laufzeit ungültig und wird wieder gültig, wenn s.car () beendet wird.
In D erhalten
Strukturelementfunktionen den Zeiger
this als Referenz, sodass wir die Ausleihe mit einer kleinen Erweiterung anpassen können: Wenn Sie das Argument als Referenz erhalten, wird es benötigt.
D unterstützt auch den Spielraum für Zeiger, sodass das Ausleihen natürlich ist:
void g(scope T*); T* f(); T* p = f(); g(p);
(Wenn Funktionen Argumente als Referenz erhalten oder Zeiger mit Gültigkeitsbereich verwendet werden, dürfen sie nicht über die Grenzen einer Funktion oder eines Gültigkeitsbereichs hinausgehen. Dies entspricht der Semantik der Ausleihe.)
Das Ausleihen auf diese Weise garantiert die Eindeutigkeit eines Zeigers auf ein Objekt im Speicher zu einem bestimmten Zeitpunkt.
Die Ausleihe kann mit dem Verständnis weiter ausgebaut werden, dass das Besitzersystem auch dann zuverlässig ist, wenn ein Objekt zusätzlich durch mehrere konstante Zeiger (aber nur einen veränderlichen) angezeigt wird. Ein konstanter Zeiger kann den Speicher nicht ändern oder freigeben. Dies bedeutet, dass mehrere konstante Zeiger vom veränderlichen Eigentümer ausgeliehen werden können, er jedoch kein Recht hat, verwendet zu werden, solange diese konstanten Zeiger am Leben sind.
Zum Beispiel:
T* f(); void g(T*); T* p = f();
Prinzipien
Das Vorstehende kann auf das folgende Verständnis reduziert werden, dass sich ein Objekt im Speicher so verhält, als ob es sich in einem von zwei Zuständen befindet:
- es gibt genau einen veränderlichen Zeiger darauf
- einen oder mehrere zusätzliche konstante Zeiger
Ein aufmerksamer Leser wird etwas Seltsames in dem bemerken, was ich geschrieben habe: "als ob". Was wollte ich andeuten? Was ist los? Ja, da ist einer. Computerprogrammiersprachen sind voll von solchen "als ob" unter der Haube, so etwas wie das Geld auf Ihrem Bankkonto ist tatsächlich nicht da (ich entschuldige mich, wenn dies ein grober Schock für jemanden war), und das ist nicht anders. Lesen Sie weiter!
Aber zuerst etwas tiefer in das Thema.
Integration von Eigentums- / Kredittechniken in D.
Sind diese Techniken nicht mit der Art und Weise unvereinbar, wie Menschen normalerweise in D schreiben, und werden nicht fast alle vorhandenen D-Programme kaputt gehen? Und es ist nicht so einfach zu beheben, aber so sehr, dass Sie alle Algorithmen von Grund auf neu entwerfen müssen?
Ja in der Tat. Es sei denn, D hat eine (fast) Geheimwaffe: Attribute von Funktionen. Es stellt sich heraus, dass die Semantik von Eigentum / Ausleihe (OB) für jede Funktion nach der üblichen semantischen Analyse separat implementiert werden kann. Ein aufmerksamer Leser könnte feststellen, dass keine neue Syntax hinzugefügt wurde, sondern nur Einschränkungen für vorhandenen Code auferlegt wurden. D hat bereits in der Vergangenheit Funktionsattribute verwendet, um ihre Semantik zu ändern, z. B. das
reine Attribut, um „reine“ Funktionen zu erstellen. Um die OB-Semantik zu aktivieren, wird das Attribut @
live hinzugefügt.
Dies bedeutet, dass der OB nach Bedarf schrittweise zum Code auf D hinzugefügt werden kann und Ressourcen frei sind. Dies ermöglicht das Hinzufügen von OBs. Dies ist von entscheidender Bedeutung, da das Projekt ständig in einem voll funktionsfähigen, getesteten und freigegebenen Zustand unterstützt wird. Außerdem können Sie den Prozess der Überwachung automatisieren, wie viel Prozent des Projekts bereits an den OB übertragen wurden. Diese Technik wird der Liste der anderen D-Sprachgarantien hinsichtlich der Zuverlässigkeit der Arbeit mit dem Speicher hinzugefügt (z. B. Steuerung der Nichtverteilung von Zeigern auf temporäre Variablen auf dem Stapel).
Als ob
Einige notwendige Dinge können nicht unter strikter Einhaltung von OBs realisiert werden, wie z. B. Referenzzählobjekte. Schließlich sind RC-Objekte so konzipiert, dass sie viele Zeiger auf sie haben. Da RC-Objekte beim Arbeiten mit Speicher sicher sind (wenn sie korrekt implementiert sind), können sie zusammen mit OBs verwendet werden, ohne die Zuverlässigkeit zu beeinträchtigen. Sie können einfach nicht mit der OB-Technik erstellt werden. Die Lösung besteht darin, dass es in D andere Funktionsattribute gibt, wie z. B. @
system . @
system sind Funktionen, bei denen viele Zuverlässigkeitsprüfungen deaktiviert sind. Natürlich wird der OB auch im Code mit @
system deaktiviert. Hier verbirgt sich die Implementierung der RC-Technologie vor der OB-Steuerung.
Aber im Code mit OB, RC sieht das Objekt so aus, als ob es allen Regeln folgt, also kein Problem!
Es werden einige ähnliche Bibliothekstypen erforderlich sein, um erfolgreich mit OB zu arbeiten.
Fazit
Dieser Artikel bietet einen grundlegenden Überblick über die OB-Technologie. Ich arbeite an einer viel detaillierteren Spezifikation. Es ist möglich, dass ich etwas verpasst habe und irgendwo ein Loch unterhalb der Wasserlinie, aber bisher sieht alles gut aus. Dies ist eine sehr aufregende Entwicklung für D und ich freue mich darauf, sie umzusetzen.
Weitere Diskussionen und Kommentare von Walter finden Sie in den Themen zu
/ r / Programming Subreddit und zu
Hacker News .