E 'possibile prevedere in modo statico quando deallocare 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 è di per sé sufficiente per sapere dove, di sicuro, una data risorsa può essere deallocata. Proprio come un programmatore umano sa dove scrivere fclose(file) quando il programma ha finito.

I GC risolvono ciò rilevandolo direttamente durante il runtime quando viene eseguito il flusso di controllo. Ma la vera fonte della verità sul flusso di controllo è la fonte. Quindi, teoricamente, dovrebbe essere possibile determinare dove inserire le chiamate free() prima della compilazione analizzando il sorgente (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 più necessari. Questo semplicemente converte la responsabilità di deallocare manualmente i puntatori a una responsabilità per gestire manualmente l'ambito / i riferimenti a quei puntatori.

Sembra che sia possibile scrivere un programma in grado di leggere l'origine 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. tiene traccia di tutti i riferimenti alle risorse allocate
  3. per ogni riferimento, attraversa tutto il successivo flusso di controllo per trovare il punto più antico in cui il riferimento è garantito per non essere mai dereferenziato
  4. a quel punto, inserisci un'istruzione di deallocation su quella riga di codice sorgente

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

    
posta zelcon5 07.03.2016 - 12:42
fonte

8 risposte

23

Prendi questo esempio (forzato):

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 gratuitamente? prima di malloc e assegniamo a resource1 non possiamo perché potrebbe essere copiato in resource2 , prima di assegnarlo a resource2 non possiamo perché potremmo aver ottenuto 2 dall'utente due volte senza intervenire 1.

L'unico modo per essere sicuri è testare resource1 e resource2 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 in cui sai che ci sono solo 2 riferimenti possibili.

    
risposta data 07.03.2016 - 13:08
fonte
27

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

Potresti considerare il problema simile "come faccio a sapere se il mio programma non subirà un errore di tipo in fase di esecuzione?". La soluzione a questo è non che prevede tutti i percorsi di esecuzione attraverso il programma ma utilizzando 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'assegnazione della memoria.

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

    
risposta data 07.03.2016 - 15:14
fonte
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 opzioni di gestione della memoria disponibili . Consente inoltre l'utilizzo di un GC convenzionale o l'ibridazione con il conteggio dei riferimenti (è possibile utilizzare un profiler dell'heap per vedere quale strategia effettivamente produrrà 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 va nei suoi successi e fallimenti. La conclusione finale è che la strategia è pratica quando si scrive con l'aiuto di un compilatore di heap.

(Questo è un buon esempio del perché di solito non si dovrebbe cercare il problema di interruzione per una risposta a domande pratiche di ingegneria: non vogliamo o abbiamo bisogno per risolvere il caso generale per la maggior parte programmi realistici).

    
risposta data 07.03.2016 - 22:19
fonte
10

predict all the permutations of the program's control flow

Questo è dove si trova il problema. La quantità di permutazioni è così grande (in pratica è infinita) per qualsiasi programma non banale, che il tempo e la memoria necessari renderebbero del tutto impraticabile.

    
risposta data 07.03.2016 - 13:14
fonte
8

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

Se hai funzioni pure o semantica di proprietà davvero buona, puoi estendere ulteriormente l'analisi statica, anche se diventa proibitivamente più costoso fare così più rami il tuo codice prende.

    
risposta data 07.03.2016 - 19:56
fonte
4

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

Tuttavia, quando si calcolano DLL di terze parti, API, framework (e anche thread), può essere molto difficile (anzi impossibile in tutti i casi) che i programmatori utilizzino correttamente il motivo dell'entità proprietaria che memoria e quando l'ultimo uso è. Il nostro solito sospetto di lingue non documenta sufficientemente il trasferimento della proprietà della memoria di oggetti e array, superficiali e profondi. Se un programmatore non riesce a ragionare su questo (staticamente o dinamicamente!), Probabilmente neanche un compilatore può farlo. Di nuovo, questo è dovuto al fatto che i trasferimenti di proprietà della memoria non vengono catturati nelle chiamate al metodo o dalle interfacce, ecc., Quindi, non è possibile prevedere in modo statico quando o dove nel codice rilasciare memoria.

Poiché questo è un problema serio, molte lingue moderne scelgono la garbage collection, che recupera automaticamente la memoria qualche volta dopo l'ultimo riferimento live. GC ha un costo significativo delle prestazioni (specialmente per le applicazioni in tempo reale), tuttavia, quindi non è una cura universale. Inoltre, puoi ancora avere perdite di memoria utilizzando 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 di proprietà nei metodi di classi e interfacce in modo più dettagliato, ad es. oggetti che vengono trasferiti, o presi in prestito, tra un chiamante e un chiamato, o in oggetti di durata più lunga. Fornisce un alto livello di sicurezza in fase di compilazione verso la gestione della memoria. Tuttavia, non è un linguaggio banale da cogliere, e non è privo di problemi (ad esempio, non penso che il design sia completamente stabile, alcune cose siano ancora sperimentate e, quindi, modificabili).

Swift e Objective-C fanno ancora un altro percorso, che è per lo più il conteggio dei riferimenti automatico. Il conteggio dei riferimenti ha problemi con i cicli e, ad esempio, ci sono sfide significative per i programmatori, specialmente con le chiusure.

    
risposta data 07.03.2016 - 17:12
fonte
2

Se un programma non dipende da input sconosciuti allora sì, dovrebbe essere possibile (con l'avvertenza che potrebbe essere un compito complesso e potrebbe richiedere molto tempo, ma ciò vale anche per il programma). Tali programmi sarebbero completamente risolvibili in fase di compilazione; in termini C ++, potrebbero essere (quasi) completamente composti da constexpr s. Esempi semplici sarebbero quelli di calcolare le prime 100 cifre di pi o di ordinare un dizionario conosciuto.

    
risposta data 08.03.2016 - 12:36
fonte
2

La liberazione della memoria, in generale, equivale al problema dell'arresto - se non si riesce a stabilire staticamente se un programma si fermerà (staticamente), non si può dire se libererà anche la memoria (staticamente).

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

link

Detto questo, Rust è molto carino ... link

    
risposta data 08.03.2016 - 13:52
fonte

Leggi altre domande sui tag