È possibile prevedere staticamente quando assegnare la memoria --- solo dal codice sorgente?


27

La memoria (e i blocchi delle risorse) vengono restituiti al sistema operativo in punti deterministici durante l'esecuzione di un programma. Il flusso di controllo di un programma da solo è sufficiente per sapere dove, di sicuro, una data risorsa può essere deallocata. Proprio come un programmatore umano sa dove scrivere al fclose(file)termine del programma.

I GC lo risolvono capendolo direttamente durante il runtime quando viene eseguito il flusso di controllo. Ma la vera fonte di verità sul flusso di controllo è la fonte. Quindi, in teoria, dovrebbe essere possibile determinare dove inserire le free()chiamate prima della compilazione analizzando l'origine (o AST).

Il conteggio dei riferimenti è un modo ovvio per implementarlo, ma è facile incontrare situazioni in cui i puntatori sono ancora referenziati (ancora nell'ambito) ma non sono più necessari. Ciò converte solo la responsabilità della deallocazione manuale dei puntatori in una responsabilità di gestire manualmente l'ambito / i riferimenti a tali puntatori.

Sembra che sia possibile scrivere un programma in grado di leggere la fonte di un programma e:

  1. prevedere tutte le permutazioni del flusso di controllo del programma --- con un'accuratezza simile a quella di guardare l'esecuzione dal vivo del programma
  2. tenere traccia di tutti i riferimenti alle risorse allocate
  3. per ogni riferimento, attraversare l'intero flusso di controllo successivo al fine di trovare il primo punto in cui è garantito che il riferimento non venga mai negato
  4. a quel punto, inserire una dichiarazione di deallocazione in quella riga di codice sorgente

C'è qualcosa là fuori che lo fa già? Non penso che i puntatori intelligenti Rust o C ++ / RAII siano la stessa cosa.


57
cercare il problema di arresto. È il nonno del perché la domanda "Non può un compilatore capire se un programma fa X?" viene sempre risposto "Non nel caso generale".
maniaco del cricchetto

18
La memoria (e i blocchi delle risorse) vengono restituiti al sistema operativo in punti deterministici durante l'esecuzione di un programma. No.
Euforico

9
@ratchetfreak Grazie, non è mai sapere cose come questo problema di arresto che mi fa desiderare di ottenere la mia laurea in scienze della scienza anziché chimica.
zelcon,

15
@ zelcon5, ora conosci la chimica e il problema dell'arresto ... :)
David Arno

7
@Euforico a meno che tu non strutturi il tuo programma in modo che i confini di quando viene utilizzata una risorsa sia molto chiaro come con RAII o try-with-resources
maniaco del cricchetto

Risposte:


23

Prendi questo esempio (inventato):

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;
    }
}

Quando dovrebbe essere chiamato free? prima di malloc e assegnarlo resource1non possiamo perché potrebbe essere copiato resource2, prima di assegnarlo resource2non possiamo perché potremmo aver ricevuto 2 utenti due volte senza intervenire 1.

L'unico modo per essere sicuri è testare la risorsa1 e la risorsa2 per vedere se non sono uguali nei casi 1 e 2 e liberare il vecchio valore se non lo fossero. Questo è essenzialmente il conteggio dei riferimenti dove sai che ci sono solo 2 possibili riferimenti.


In realtà non è l'unico modo; l'altro modo è consentire l' esistenza di una sola copia. Questo, ovviamente, ha i suoi problemi.
Jack Aidley l'

27

RAII non è automaticamente la stessa cosa, ma ha lo stesso effetto. Fornisce una risposta semplice alla domanda "come fai a sapere quando non è più possibile accedervi?" usando l' ambito per coprire l'area quando viene utilizzata una particolare risorsa.

Potresti voler considerare il problema simile "come posso sapere se il mio programma non subirà un errore di tipo in fase di esecuzione?". La soluzione a questo non è prevedere tutti i percorsi di esecuzione attraverso il programma, ma usando un sistema di annotazione e inferenza del tipo per dimostrare che non può esserci un tale errore. Rust è un tentativo di estendere questa proprietà di prova all'allocazione di memoria.

È possibile scrivere prove sul comportamento del programma senza dover risolvere il problema di arresto, ma solo se si utilizzano annotazioni di qualche tipo per vincolare il programma. Vedi anche prove di sicurezza (sel4 ecc.)


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
maple_shaft

13

Sì, questo esiste in natura. ML Kit è un compilatore di qualità di produzione che ha la strategia descritta (più o meno) come una delle sue opzioni di gestione della memoria disponibili. Consente inoltre l'uso di un GC convenzionale o l'ibridazione con il conteggio dei riferimenti (è possibile utilizzare un profilatore heap per vedere quale strategia produrrà effettivamente i risultati migliori per il proprio programma).

Una retrospettiva sulla gestione della memoria basata sulla regione è un articolo degli autori originali del kit ML che illustra i suoi successi e fallimenti. L'eventuale conclusione è che la strategia è pratica quando si scrive con l'assistenza di un profilatore di heap.

(Questa è una buona illustrazione del perché di solito non dovresti guardare al problema Halting per una risposta a domande pratiche di ingegneria: non vogliamo o non dobbiamo risolvere il caso generale per la maggior parte dei programmi realistici.)


5
Penso che questo sia un eccellente esempio di corretta applicazione del problema Halting. Il problema di arresto ci dice che il problema è irrisolvibile nel caso generale, quindi si cercano scenari limitati in cui il problema è risolvibile.
Taemyr,

Si noti che il problema diventa molto più risolvibile quando parliamo di linguaggi puri o quasi puri con effetti collaterali come Standard ML e Haskell
cat

10

prevedere tutte le permutazioni del flusso di controllo del programma

Questo è dove sta il problema. La quantità di permutazioni è così grande (in pratica è infinita) per qualsiasi programma non banale, che il tempo e la memoria necessari renderebbero tutto ciò poco pratico.


buon punto. Immagino che i processori quantistici siano l'unica speranza, se ce ne sono affatto
zelcon

4
@ zelcon5 Haha, no. Il calcolo quantistico rende questo peggio , non migliore. Aggiunge ulteriori variabili ("nascoste") al programma e molta più incertezza. Il codice QC più pratico che ho visto si basa su "quantum per un calcolo veloce, classico per conferma". Mi sono appena sfiorato la superficie sul calcolo quantistico, ma mi sembra che i computer quantistici potrebbero non essere molto utili senza i computer classici per eseguirne il backup e verificarne i risultati.
Luaan,

8

Il problema di arresto dimostra che ciò non è possibile in tutti i casi. Tuttavia, è ancora possibile in molti casi, e in effetti è fatto da quasi tutti i compilatori per la maggior parte delle variabili. Questo è il modo in cui un compilatore può dire che è sicuro semplicemente allocare una variabile nello stack o persino un registro, invece che nella memoria heap a lungo termine.

Se si hanno funzioni pure o una semantica di proprietà davvero buona, è possibile estendere ulteriormente tale analisi statica, anche se diventa proibitivamente più costoso farlo, più rami prende il codice.


Bene, il compilatore pensa di poter liberare la memoria; ma potrebbe non essere così. Pensa all'errore del principiante comune di restituire un puntatore o un riferimento a una variabile locale. I casi banali vengono colti dal compilatore, vero; quelli meno banali non lo sono.
Peter - Ripristina Monica l'

Tale errore viene commesso dai programmatori in lingue in cui i programmatori devono gestire manualmente l'allocazione della memoria @Peter. Quando il compilatore gestisce l'allocazione della memoria, questo tipo di errori non si verificano.
Karl Bielefeldt,

Bene, hai fatto una dichiarazione molto generale includendo la frase "quasi tutti i compilatori" che deve includere compilatori C.
Peter - Ripristina Monica l'

2
I compilatori C lo usano per determinare quali variabili temporanee possono essere allocate ai registri.
Karl Bielefeldt,

4

Se un singolo programmatore o team scrive l'intero programma, è ragionevole identificare i punti di progettazione in cui liberare la memoria (e altre risorse). Pertanto, sì, l'analisi statica del progetto può essere sufficiente in contesti più limitati.

Tuttavia, quando si tiene conto di DLL, API, framework (e anche di thread) di terze parti, può essere molto difficile (anzi, impossibile in tutti i casi) per i programmatori che usano ragionare correttamente su quale entità possiede quale memoria e quando l'ultimo è. Il nostro solito sospettato di lingue non documenta sufficientemente il trasferimento della proprietà della memoria di oggetti e matrici, superficiali e profonde. Se un programmatore non può ragionare su questo (staticamente o dinamicamente!), Molto probabilmente neanche un compilatore può farlo. Ancora una volta, ciò è dovuto al fatto che i trasferimenti di proprietà della memoria non vengono acquisiti nelle chiamate di metodo o da interfacce, ecc., Quindi, non è possibile prevedere staticamente quando o dove nel codice rilasciare memoria.

Poiché si tratta di un problema così grave, molte lingue moderne scelgono la garbage collection, che recupera automaticamente la memoria dopo l'ultimo riferimento live. GC ha un costo di prestazioni significativo (soprattutto per applicazioni in tempo reale), tuttavia non è una cura universale. Inoltre, puoi ancora avere perdite di memoria usando GC (ad esempio una raccolta che cresce solo). Tuttavia, questa è una buona soluzione per la maggior parte degli esercizi di programmazione.

Ci sono alcune alternative (alcune emergenti).

Il linguaggio Rust porta RAII all'estremo. Fornisce costrutti linguistici che definiscono il trasferimento della proprietà in metodi di classi e interfacce in modo più dettagliato, ad esempio oggetti che vengono trasferiti o presi in prestito da un chiamante e chiamato, o in oggetti a vita più lunga. Fornisce un elevato livello di sicurezza in fase di compilazione verso la gestione della memoria. Tuttavia, non è un linguaggio banale da imparare, e non è anche privo di problemi (ad esempio, non penso che il design sia completamente stabile, alcune cose sono ancora in fase di sperimentazione e, quindi, cambiano).

Swift e Objective-C seguono ancora un'altra strada, che è il conteggio dei riferimenti prevalentemente automatico. Il conteggio dei riferimenti causa problemi con i cicli e, ad esempio, ci sono sfide significative per i programmatori, in particolare con le chiusure.


3
Certo, GC ha dei costi, ma ha anche vantaggi in termini di prestazioni. Ad esempio, su .NET, l'allocazione dall'heap è quasi gratuita, perché utilizza il modello di "allocazione dello stack": basta incrementare un puntatore e basta. Ho visto applicazioni che girano più velocemente riscritte nel GC .NET di quanto non stiano usando l'allocazione manuale della memoria, non è davvero chiaro. Allo stesso modo, il conteggio dei riferimenti è in realtà piuttosto costoso (solo in luoghi diversi da un GC) e qualcosa che non vuoi pagare se puoi evitarlo. Se desideri prestazioni in tempo reale, l'allocazione statica è spesso l'unico modo.
Luaan,

2

Se un programma non dipende da alcun input sconosciuto, allora sì, dovrebbe essere possibile (con l'avvertenza che potrebbe essere un compito complesso e potrebbe richiedere molto tempo, ma ciò sarebbe vero anche per il programma). Tali programmi sarebbero completamente risolvibili al momento della compilazione; in termini di C ++, potrebbero essere (quasi) completamente composti da constexprs. Semplici esempi potrebbero essere il calcolo delle prime 100 cifre di pi o l'ordinamento di un dizionario noto.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.