Svantaggi della gestione della memoria basata su scope

37

Mi piace molto la gestione della memoria basata su scope (SBMM), o RAII , poiché è più comunemente (in modo confuso?) a cui fa riferimento la comunità C ++. Per quanto ne so, ad eccezione di C ++ (e C), oggi non esiste un altro linguaggio mainstream in uso che faccia di SBMM / RAII il loro meccanismo principale di gestione della memoria, e invece preferiscono usare la garbage collection (GC).

Trovo questo piuttosto confuso, dal momento che

  1. SBMM rende i programmi più deterministici (puoi dire esattamente quando un oggetto viene distrutto);
  2. nelle lingue che usano GC spesso devi fare gestione manuale delle risorse (vedi la chiusura di file in Java, per esempio), che in parte vanifica lo scopo di GC ed è anche soggetto a errori;
  3. memoria heap può anche (molto elegantemente, imo) essere limitato dall'ambito (vedere std::shared_ptr in C ++).

Perché SBMM non è più ampiamente utilizzato? Quali sono i suoi svantaggi?

    
posta Paul 09.03.2014 - 14:32
fonte

8 risposte

27

Iniziamo postulando che la memoria è di gran lunga più comune (dozzine, centinaia o addirittura migliaia di volte) rispetto a tutte le altre risorse combinate. Ogni singola variabile, oggetto, membro dell'oggetto ha bisogno di memoria assegnata e liberata in seguito. Per ogni file che apri, crei dozzine di milioni di oggetti per archiviare i dati estratti dal file. Ogni flusso TCP va insieme a un numero illimitato di stringhe di byte temporanee create per essere scritte nello stream. Siamo sulla stessa pagina qui? Grande.

Per far funzionare RAII (anche se hai puntatori intelligenti già pronti per ogni caso d'uso sotto il sole), devi ottenere il diritto proprietà . Devi analizzare chi dovrebbe possedere questo o quell'oggetto, chi non dovrebbe, e quando la proprietà dovrebbe essere trasferita da A a B. Certo, potresti usare la proprietà condivisa per tutto , ma poi dovresti emulare un GC tramite puntatori intelligenti. A quel punto diventa molto più facile e più veloce per costruire il GC nella lingua.

La raccolta dei dati inutili ti libera da questa preoccupazione per la risorsa, la memoria, di gran lunga più utilizzata. Certo, devi ancora prendere la stessa decisione per altre risorse, ma quelle sono molto meno comuni (vedi sopra), e anche la proprietà complicata (ad esempio condivisa) è meno comune. Il carico mentale è significativamente ridotto.

Ora, dai il nome di alcuni aspetti negativi a rendere i tutti valori garbage collection. Tuttavia, l'integrazione di entrambi i tipi di valore GC e con RAII in una sola lingua è estremamente difficile, quindi forse è meglio migare questi trade off con altri mezzi?

La perdita di determinismo risulta non essere così male nella pratica, perché influenza solo la durata dell'oggetto deterministica. Come descritto nel prossimo paragrafo, la maggior parte delle risorse (oltre alla memoria, che è abbondante e può essere riciclata piuttosto pigramente) sono non associate alla durata dell'oggetto in queste lingue. Ci sono alcuni altri casi d'uso, ma sono rari nella mia esperienza.

Il tuo secondo punto, la gestione manuale delle risorse, è attualmente affrontato tramite una dichiarazione che esegue la pulizia basata sull'ambito, ma non accoppiato alla pulizia dell'oggetto (quindi non all'interazione con GC e sicurezza della memoria). Questo è using in C #, with in Python, try -with-resources nelle ultime versioni di Java.

    
risposta data 09.03.2014 - 15:13
fonte
14

RAII segue anche dalla gestione automatica della memoria di conteggio dei riferimenti, ad es. come usato da Perl. Mentre il conteggio dei riferimenti è facile da implementare, deterministico e abbastanza performante, non può gestire riferimenti circolari (causano una perdita), motivo per cui non è comunemente usato.

Le lingue raccolte da garbage non possono usare RAII direttamente, ma spesso offrono sintassi con un effetto equivalente. In Java, abbiamo l'istruzione try-with-ressource

try (BufferedReader br = new BufferedReader(new FileReader(path))) { ... }

che chiama automaticamente .close() sulla risorsa all'uscita del blocco. C # ha l'interfaccia IDisposable , che consente di chiamare .Dispose() quando si esce da un'istruzione using (...) { ... } . Python ha l'istruzione with :

with open(filename) as f:
    ...

che funziona in modo simile. In un interessante giro su questo, il metodo di apertura dei file di Ruby ottiene una richiamata. Dopo che la richiamata è stata eseguita, il file è chiuso.

File.open(name, mode) do |f|
    ...
end

Penso che Node.js usi la stessa strategia.

    
risposta data 09.03.2014 - 15:01
fonte
14

A mio parere, il vantaggio più convincente della garbage collection è che consente composability . La correttezza della gestione della memoria è una proprietà locale nell'ambiente garbage collocato. È possibile esaminare ciascuna parte isolatamente e determinare se può perdere memoria. Combina qualsiasi numero di parti corrette per la memoria e rimangono corretti.

Quando ti affidi al conteggio dei riferimenti perdi quella proprietà. Se la tua applicazione può perdere memoria, diventa una proprietà globale dell'intera applicazione con il conteggio dei riferimenti. Ogni nuova interazione tra le parti ha la possibilità di utilizzare la proprietà sbagliata e interrompere la gestione della memoria.

Ha un effetto molto visibile sulla progettazione dei programmi nelle diverse lingue. I programmi nei linguaggi GC tendono ad essere un po 'più di zuppe di oggetti con molte interazioni, mentre nei linguaggi GC-less si tende a preferire parti strutturate con interazioni strettamente controllate e limitate tra loro.

    
risposta data 09.03.2014 - 16:36
fonte
7

Le chiusure sono una caratteristica essenziale di quasi tutte le lingue moderne. Sono molto facili da implementare con GC e sono molto difficili (anche se non impossibili) per funzionare correttamente con RAII, dato che una delle loro caratteristiche principali è che ti permettono di astrarre per tutta la durata delle tue variabili!

C ++ li ha ottenuti solo 40 anni dopo che tutti gli altri l'hanno fatto, e ci sono voluti un sacco di duro lavoro da un sacco di persone intelligenti per farli bene. Al contrario, molti linguaggi di scripting progettati e implementati da persone con zero conoscenze nella progettazione e implementazione di linguaggi di programmazione li hanno.

    
risposta data 09.03.2014 - 15:13
fonte
5
  1. SBMM makes programs more deterministic (you can tell exactly when an object is destroyed);

Per la maggior parte dei programmatori il sistema operativo non è deterministico, il loro allocatore di memoria è non deterministico e la maggior parte dei programmi che scrivono sono concomitanti e, quindi, intrinsecamente non deterministici. L'aggiunta del vincolo che un distruttore viene chiamato esattamente alla fine dell'ambito piuttosto che leggermente prima o poco dopo non è un vantaggio pratico significativo per la stragrande maggioranza dei programmatori.

  1. in languages that use GC you often have to do manual resource management (see closing files in Java, for example), which partly defeats the purpose of GC and is also error prone;

Visualizza using in C # e use in F #.

  1. heap memory can also (very elegantly, imo) be scope-bound (see std::shared_ptr in C++).

In altre parole, puoi prendere l'heap che è una soluzione generica e cambiarlo per funzionare solo in un caso specifico che è seriamente limitante. Questo è vero, ovviamente, ma inutile.

Why is not SBMM more widely used? What are its disadvantages?

SBMM limita ciò che puoi fare:

  1. SBMM crea il problema del funarg upward con chiusure lessicali di prima classe, motivo per cui le chiusure sono popolari e facili da usare in linguaggi come C # ma raro e complicato in C ++. Si noti che esiste una tendenza generale verso l'uso di costrutti funzionali nella programmazione.

  2. SBMM richiede i distruttori e impediscono le chiamate di coda aggiungendo altro lavoro da fare prima che una funzione possa tornare. Le chiamate tail sono utili per le macchine a stati estensibili e sono fornite da cose come .NET.

  3. Alcune strutture dati e algoritmi sono notoriamente difficili da implementare con SBMM. Fondamentalmente ovunque i cicli si verificano naturalmente. In particolare algoritmi di grafi. In pratica finisci per scrivere il tuo GC.

  4. La programmazione simultanea è più difficile perché il flusso di controllo e, di conseguenza, le durate degli oggetti sono intrinsecamente non deterministici qui. Le soluzioni pratiche nei sistemi di trasmissione dei messaggi tendono ad essere una copia profonda dei messaggi e l'uso di vite eccessivamente lunghe.

  5. SBMM mantiene gli oggetti in vita fino alla fine del loro ambito nel codice sorgente, che è spesso più lungo del necessario e può essere molto più lungo del necessario. Ciò aumenta la quantità di rifiuti mobili (oggetti non raggiungibili in attesa di essere riciclati). Al contrario, la raccolta dei dati inutili tende a liberare oggetti subito dopo la scomparsa dell'ultimo riferimento a essi, che può essere molto più rapido. Vedi Miti di gestione della memoria: prompt .

SBMM è così limitante che i programmatori hanno bisogno di una via di fuga per le situazioni in cui non è possibile fare annidare le vite. In C ++, shared_ptr offre una via di fuga ma it può essere ~ 10 volte più lento della traccia di garbage collection . Quindi usare SBMM invece di GC metterebbe la maggior parte delle persone in errore nella maggior parte dei casi. Ciò non vuol dire, tuttavia, che sia inutile. SBMM ha ancora valore nel contesto dei sistemi e della programmazione incorporata in cui le risorse sono limitate.

FWIW potresti controllare Forth e Ada e leggere il lavoro di Nicolas Wirth.

    
risposta data 13.01.2015 - 08:10
fonte
4

Guardando un indice di popolarità come TIOBE (che è discutibile, ovviamente, ma suppongo che per il tuo tipo di domanda è ok per usarlo), per prima cosa vedi che ~ 50% dei primi 20 sono "linguaggi di scripting" o "Dialetti SQL", dove la "facilità d'uso" e i mezzi di astrazione hanno un'importanza molto maggiore del comportamento deterministico. Dalle restanti lingue "compilate", ci sono circa il 50% delle lingue con SBMM e ~ 50% senza. Quindi, quando estrai i linguaggi di scripting dal tuo calcolo, direi che la tua ipotesi è semplicemente sbagliata, tra le lingue compilate quelle con SBMM sono così popolari come quelle senza.

    
risposta data 09.03.2014 - 15:11
fonte
3

Uno dei principali vantaggi di un sistema GC che nessuno ha ancora menzionato è che un riferimento in un sistema GC è garantito per mantenere la sua identità finché esiste . Se si chiama IDisposable.Dispose (.NET) o AutoCloseable.Close (Java) su un oggetto mentre esistono copie del riferimento, tali copie continueranno a riferirsi allo stesso oggetto. L'oggetto non sarà più utile per nulla, ma i tentativi di usarlo avranno un comportamento prevedibile controllato dall'oggetto stesso. Al contrario, in C ++, se il codice chiama delete su un oggetto e successivamente tenta di utilizzarlo, l'intero stato del sistema diventa totalmente indefinito.

Un'altra cosa importante da notare è che la gestione della memoria basata su scope funziona molto bene per oggetti con proprietà chiaramente definite. Funziona molto meno bene, e talvolta addirittura male, con oggetti che non hanno una proprietà definita. In generale, gli oggetti mutabili dovrebbero avere proprietari, mentre gli oggetti non modificabili non ne hanno bisogno, ma c'è una ruga: è molto comune che il codice usi un'istanza di tipi mutabili per conservare dati immutabili, garantendo che nessun riferimento sia esposto a codice che potrebbe mutare l'istanza. In tale scenario, le istanze della classe mutabile potrebbero essere condivise tra più oggetti immutabili e quindi non avere una chiara proprietà.

    
risposta data 10.03.2014 - 01:11
fonte
-2

Prima di tutto, è molto importante rendersi conto che equiparare RAII a SBMM. o anche a SBRM. Una delle qualità più essenziali (e meno conosciute o più apprezzate) di RAII è il fatto che rende "essere una risorsa" una proprietà che NON è transitiva alla composizione.

Il seguente post sul blog discute questo importante aspetto di RAII e lo mette in contrasto con l'ammodernamento delle risorse nei linguaggi GCed che utilizzano GC non deterministico.

link

È importante notare che mentre RAII è usato principalmente in C ++, Python (finalmente la versione non basata su VM) ha distruttori e GC deterministici che permettono a RAII di essere usato insieme a GC. Il meglio di entrambi i mondi se lo fosse.

    
risposta data 15.03.2014 - 00:27
fonte