it-swarm.com.de

Ist es möglich, statisch vorherzusagen, wann Speicher freigegeben werden soll - nur aus dem Quellcode?

Speicher (und Ressourcensperren) werden an deterministischen Punkten während der Ausführung eines Programms an das Betriebssystem zurückgegeben. Der Kontrollfluss eines Programms allein reicht aus, um zu wissen, wo eine bestimmte Ressource sicher freigegeben werden kann. Genau wie ein menschlicher Programmierer weiß, wo er fclose(file) schreiben muss, wenn das Programm damit fertig ist.

GCs lösen dieses Problem, indem sie es direkt zur Laufzeit herausfinden, wenn der Kontrollfluss ausgeführt wird. Aber die wahre Quelle der Wahrheit über den Kontrollfluss ist die Quelle. Theoretisch sollte es also möglich sein, zu bestimmen, wo die Aufrufe von free() vor dem Kompilieren eingefügt werden sollen, indem die Quelle (oder AST) analysiert wird.

Das Zählen von Referenzen ist ein offensichtlicher Weg, um dies zu implementieren, aber es ist leicht, Situationen zu begegnen, in denen Zeiger immer noch referenziert werden (noch im Geltungsbereich), aber nicht mehr benötigt werden. Dadurch wird lediglich die Verantwortung für die manuelle Freigabe von Zeigern in eine Verantwortung für die manuelle Verwaltung des Bereichs/der Verweise auf diese Zeiger umgewandelt.

Es scheint möglich zu sein, ein Programm zu schreiben, das die Quelle eines Programms lesen kann und:

  1. vorhersage aller Permutationen des Kontrollflusses des Programms - mit ähnlicher Genauigkeit wie die Live-Ausführung des Programms
  2. verfolgen Sie alle Verweise auf zugewiesene Ressourcen
  3. durchlaufen Sie für jede Referenz den gesamten nachfolgenden Kontrollfluss, um den frühesten Punkt zu finden, an dem die Referenz garantiert niemals dereferenziert wird
  4. fügen Sie zu diesem Zeitpunkt eine Freigabeanweisung in diese Zeile des Quellcodes ein

Gibt es da draußen irgendetwas, das das schon macht? Ich denke nicht, dass Rust oder C++ Smart Pointers/RAII dasselbe ist.

27
zelcon

Nehmen Sie dieses (erfundene) Beispiel:

void* resource1;
void* resource2;

while(true){

    int input = getInputFromUser();

    switch(input){
        case 1: resource1 = malloc(500); break;
        case 2: resource2 = resource1; break;
        case 3: useResource(resource1); useResource(resource2); break;
    }
}

Wann soll free angerufen werden? vor malloc und zuweisen zu resource1 können wir nicht, weil es möglicherweise zu resource2 kopiert wird, vor zuweisen zu resource2 können wir nicht, weil wir möglicherweise 2 vom Benutzer erhalten haben zweimal ohne dazwischen 1.

Die einzige Möglichkeit, sicher zu sein, besteht darin, Ressource1 und Ressource2 zu testen, um festzustellen, ob sie in den Fällen 1 und 2 nicht gleich sind, und den alten Wert freizugeben, wenn dies nicht der Fall ist. Dies ist im Wesentlichen eine Referenzzählung, bei der Sie wissen, dass es nur zwei mögliche Referenzen gibt.

23
ratchet freak

RAII ist nicht automatisch dasselbe, hat aber den gleichen Effekt. Es bietet eine einfache Antwort auf die Frage "Woher wissen Sie, wann darauf nicht mehr zugegriffen werden kann?" Verwenden Sie scope, um den Bereich abzudecken, in dem eine bestimmte Ressource verwendet wird.

Vielleicht möchten Sie das ähnliche Problem in Betracht ziehen: "Woher weiß ich, dass mein Programm zur Laufzeit keinen Typfehler aufweist?". Die Lösung hierfür ist nicht die Vorhersage aller Ausführungspfade durch das Programm, aber die Verwendung eines Systems mit Typanmerkungen und Schlussfolgerungen, um zu beweisen, dass es keinen solchen Fehler geben kann. Rust ist ein Versuch, diese Proof-Eigenschaft auf die Speicherzuordnung zu erweitern.

Es ist möglich, Beweise über das Programmverhalten zu schreiben, ohne das Problem des Anhaltens lösen zu müssen, aber nur, wenn Sie Anmerkungen verwenden, um das Programm einzuschränken. Siehe auch Sicherheitsnachweise (sel4 etc.)

27
pjc50

Ja, das gibt es in freier Wildbahn. The ML Kit ist ein Compiler in Produktionsqualität, der die beschriebene Strategie (mehr oder weniger) als eine seiner verfügbaren Speicherverwaltungsoptionen verwendet. Es ermöglicht auch die Verwendung eines herkömmlichen GC oder die Hybridisierung mit Referenzzählung (Sie können einen Heap-Profiler verwenden, um zu sehen, welche Strategie tatsächlich die besten Ergebnisse für Ihr Programm liefert).

Eine Retrospektive zur regionalen Speicherverwaltung ist ein Artikel der ursprünglichen Autoren des ML-Kits, der sich mit seinen Erfolgen und Misserfolgen befasst. Die endgültige Schlussfolgerung ist, dass die Strategie praktisch ist, wenn mit Hilfe eines Heap-Profilers geschrieben wird.

(Dies ist ein gutes Beispiel dafür, warum Sie normalerweise nicht auf das Halteproblem schauen sollten, um eine Antwort auf praktische technische Fragen zu erhalten: Wir wollen oder brauchen nicht um den allgemeinen Fall für die realistischsten Programme zu lösen.)

13
Leushenko

vorhersage aller Permutationen des Kontrollflusses des Programms

Hier liegt das Problem. Die Anzahl der Permutationen ist für jedes nicht triviale Programm so groß (in der Praxis ist es unendlich), dass Zeit und Speicherbedarf dies völlig unpraktisch machen würden.

10
Euphoric

Das Stoppproblem beweist, dass dies nicht in allen Fällen möglich ist. Es ist jedoch in sehr vielen Fällen immer noch möglich und wird tatsächlich von fast allen Compilern für wahrscheinlich die Mehrheit der Variablen durchgeführt. Auf diese Weise kann ein Compiler feststellen, dass es sicher ist, lediglich eine Variable auf dem Stapel oder sogar einem Register zuzuweisen, anstatt einem längerfristigen Heapspeicher.

Wenn Sie über reine Funktionen oder eine wirklich gute Besitzersemantik verfügen, können Sie diese statische Analyse weiter ausbauen, obwohl dies umso teurer wird, je mehr Verzweigungen Ihr Code benötigt.

8
Karl Bielefeldt

Wenn ein einzelner Programmierer oder ein Team das gesamte Programm schreibt, ist es sinnvoll, Entwurfspunkte zu identifizieren, an denen Speicher (und andere Ressourcen) freigegeben werden sollten. Daher kann eine statische Analyse des Entwurfs in engeren Kontexten ausreichend sein.

Wenn Sie jedoch DLLs, APIs, Frameworks von Drittanbietern berücksichtigen (und auch Threads einwerfen), kann es für die verwendenden Programmierer sehr schwierig (ja in allen Fällen unmöglich) sein, richtig zu überlegen, welche Entität welchen Speicher besitzt und wenn die letzte Verwendung davon ist. Unser üblicher Sprachverdächtiger dokumentiert die Übertragung des Speicherbesitzes von flachen und tiefen Objekten und Arrays nicht ausreichend. Wenn ein Programmierer nicht darüber nachdenken kann (statisch oder dynamisch!), Kann ein Compiler dies höchstwahrscheinlich auch nicht. Dies ist wiederum auf die Tatsache zurückzuführen, dass Speicherübertragungen nicht in Methodenaufrufen oder durch Schnittstellen usw. erfasst werden. Daher ist es nicht möglich, statisch vorherzusagen, wann oder wo im Code Speicher freigegeben werden soll.

Da dies ein so ernstes Problem darstellt, entscheiden sich viele moderne Sprachen für die Speicherbereinigung, die nach der letzten Live-Referenz automatisch Speicher zurückgewinnt. GC hat jedoch erhebliche Leistungskosten (insbesondere für Echtzeitanwendungen) und ist daher kein universelles Allheilmittel. Darüber hinaus können mit GC immer noch Speicherverluste auftreten (z. B. eine Sammlung, die nur wächst). Dies ist jedoch eine gute Lösung für die meisten Programmierübungen.

Es gibt einige Alternativen (einige entstehen).

Die Sprache Rust) bringt RAII auf ein Extrem. Sie bietet sprachliche Konstrukte, die die Übertragung des Eigentums an Methoden von Klassen und Schnittstellen detaillierter definieren, z. B. Objekte, auf die zwischen übertragen oder ausgeliehen werden Anrufer und Angerufene oder Objekte mit längerer Lebensdauer. Es bietet ein hohes Maß an Kompilierungszeitsicherheit für die Speicherverwaltung. Es ist jedoch keine triviale Sprache zum Aufnehmen und auch nicht ohne Probleme (z. B. glaube ich nicht Das Design ist völlig stabil, bestimmte Dinge werden noch experimentiert und ändern sich daher.

Swift und Objective-C gehen noch einen anderen Weg, nämlich die meist automatische Referenzzählung. Die Referenzzählung gerät in Probleme mit Zyklen, und es gibt erhebliche Herausforderungen für Programmierer, insbesondere bei Schließungen.

4
Erik Eidt

Wenn ein Programm nicht von einer unbekannten Eingabe abhängt, sollte dies möglich sein (mit der Einschränkung, dass es sich um eine komplexe Aufgabe handelt und lange dauern kann; dies gilt jedoch auch für das Programm). Solche Programme wären zum Zeitpunkt der Kompilierung vollständig lösbar. In C++ könnten sie (fast) vollständig aus constexprs bestehen. Einfache Beispiele wären, die ersten 100 Stellen von pi zu berechnen oder ein bekanntes Wörterbuch zu sortieren.

Das Freigeben von Speicher entspricht im Allgemeinen dem Problem des Anhaltens. Wenn Sie nicht statisch feststellen können, ob ein Programm (statisch) angehalten wird, können Sie auch nicht feststellen, ob es (statisch) Speicher freigibt.

function foo(int a) {
    void *p = malloc(1);
    ... do something which may, or may not, halt ...
    free(p);
}

https://en.wikipedia.org/wiki/Halting_problem

Das heißt, Rust ist sehr schön ... https://doc.Rust-lang.org/book/ownership.html

2
fadedbee