Dimostrazione di garbage collection più veloce della gestione manuale della memoria


23

Ho letto in molti posti (diamine, l'ho anche scritto io stesso) che la raccolta dei rifiuti potrebbe (teoricamente) essere più veloce della gestione manuale della memoria.

Tuttavia, mostrare è molto più difficile che dirlo.
In realtà non ho mai visto alcun pezzo di codice che dimostri questo effetto in azione.

Qualcuno ha (o sa dove posso trovare) il codice che dimostra questo vantaggio prestazionale?


5
il problema con GC è che la maggior parte delle implementazioni non sono deterministiche, quindi 2 esecuzioni possono avere risultati molto diversi, per non parlare del fatto che è difficile isolare le variabili giuste da confrontare
maniaco del cricchetto

@ratchetfreak: se conosci esempi che sono solo più veloci (diciamo) il 70% delle volte, anche per me va bene. Ci deve essere un modo per confrontare i due, almeno in termini di throughput (la latenza probabilmente non funzionerebbe).
Mehrdad,

1
Bene, questo è un po 'complicato perché puoi sempre fare manualmente tutto ciò che dà al GC un vantaggio rispetto a quello che hai fatto manualmente. Forse è meglio limitare questo a strumenti di gestione della memoria manuale "standard" (malloc () / free (), puntatori di proprietà, puntatori condivisi con refcount, puntatori deboli, nessun allocatore personalizzato)? Oppure, se si autorizzano allocatori personalizzati (che possono essere più realistici o meno realistici, a seconda del tipo di programmatore che si assume), porre restrizioni allo sforzo messo in quegli allocatori. Altrimenti, la strategia manuale "copia ciò che fa il GC in questo caso" è sempre veloce almeno quanto il GC.

1
Con "copia di ciò che fa il GC" non intendevo "costruire il tuo GC" (sebbene si noti che ciò è teoricamente possibile in C ++ 11 e oltre, che introduce il supporto opzionale per un GC). Intendevo, come ho già scritto in precedenza nello stesso commento, "fare ciò che dà al GC un vantaggio rispetto a quello che hai fatto manualmente". Ad esempio, se la compattazione simile a Cheney aiuta molto questa applicazione, è possibile implementare manualmente uno schema di allocazione + compattazione simile, con puntatori intelligenti personalizzati per gestire la correzione del puntatore. Inoltre, con tecniche come una pila di ombre, è possibile eseguire la ricerca di root in C o C ++, a spese del lavoro extra.

1
@Ike: va bene. Vedi perché ho fatto la domanda però? Questo era il punto centrale della mia domanda: le persone escogitano ogni sorta di spiegazioni che dovrebbero avere senso, ma tutti inciampano quando chiedi loro di fornire una dimostrazione che dimostri che ciò che dicono è corretto nella pratica. L'intero punto di questa domanda era dimostrare una volta per tutte che ciò può effettivamente accadere in pratica.
Mehrdad,

Risposte:


26

Vedi http://blogs.msdn.com/b/ricom/archive/2005/05/10/416151.aspx e segui tutti i link per vedere Rico Mariani vs Raymond Chen (entrambi programmatori molto competenti di Microsoft) mentre lo duella . Raymond avrebbe migliorato quello non gestito, Rico avrebbe risposto ottimizzando la stessa cosa in quelli gestiti.

Con uno sforzo di ottimizzazione sostanzialmente nullo, le versioni gestite sono iniziate molte volte più velocemente del manuale. Alla fine il manuale supera quello gestito, ma solo ottimizzando a un livello al quale la maggior parte dei programmatori non vorrebbe andare. In tutte le versioni, l'utilizzo della memoria del manuale era significativamente migliore di quello gestito.


+1 per citare un esempio reale con il codice :) anche se l'uso corretto dei costrutti C ++ (come swap) non è così difficile e probabilmente ti porterebbe abbastanza facilmente dal punto di vista delle prestazioni ...
Mehrdad

5
Potresti essere in grado di superare Raymond Chen in termini di prestazioni. Sono fiducioso di non poterlo fare a meno che non ne sia fuori a causa di una malattia, sto lavorando molte volte di più e sono stato fortunato. Non so perché non abbia scelto la soluzione che avresti scelto. Sono sicuro che ne avesse delle ragioni
btilly

Ho copiato il codice di Raymond qui , e per confrontare, ho scritto la mia versione qui . Il file ZIP che contiene il file di testo è qui . Sul mio computer, il mio funziona in 14 ms e il raggio di Raymond in 21 ms. A meno che non abbia fatto qualcosa di sbagliato (il che è possibile), il suo codice a 215 righe è più lento del 50% rispetto alla mia implementazione a 48 righe, anche senza utilizzare file mappati in memoria o pool di memoria personalizzati (che ha usato). Il mio è lungo la metà della versione C #. Ho sbagliato o osservi la stessa cosa?
Mehrdad,

1
@Mehrdad Tirando fuori una vecchia copia di gcc su questo laptop posso segnalare che né il tuo codice né i suoi verranno compilati, figuriamoci fare qualcosa con esso. Il fatto che io non sia su Windows probabilmente lo spiega. Ma supponiamo che i tuoi numeri e codice siano corretti. Si comportano allo stesso modo su un compilatore e un computer decennali? (Guarda quando è stato scritto il blog.) Forse, forse no. Supponiamo che lo siano, che lui (essendo un programmatore C) non sapeva come usare correttamente C ++, ecc. Cosa ci rimane?
btilly

1
Ci resta un ragionevole programma C ++ che può essere tradotto in memoria gestita e velocizzato. Ma dove la versione C ++ può essere ottimizzata e velocizzata ulteriormente. Ciò che siamo tutti d'accordo è il modello generale che accade sempre quando il codice gestito è più veloce di quello non gestito. Comunque abbiamo ancora un esempio concreto di codice ragionevole da un buon programmatore che era più veloce in una versione gestita.
btilly

5

La regola empirica è che non ci sono pranzi gratuiti.

GC elimina il mal di testa della gestione manuale della memoria e riduce la probabilità di errori. Ci sono alcune situazioni in cui una particolare strategia GC è la soluzione ottimale per il problema, nel qual caso non pagherai alcuna penalità per l'utilizzo. Ma ce ne sono altri in cui altre soluzioni saranno più veloci. Dato che puoi sempre simulare astrazioni più alte da un livello inferiore, ma non viceversa, puoi effettivamente dimostrare che non è possibile che le astrazioni più alte siano più veloci di quelle inferiori nel caso generale.

GC è un caso speciale di gestione manuale della memoria

Potrebbe essere necessario molto lavoro o più soggetto a errori per ottenere prestazioni migliori manualmente ma questa è una storia diversa.


1
Non ha senso per me. Per darvi un paio di esempi concreti: 1) gli allocatori e le barriere di scrittura nei GC di produzione sono assemblatori scritti a mano perché C è troppo inefficiente, quindi come lo batterete da C, e 2) l'eliminazione della coda è un esempio di ottimizzazione fatto in linguaggi di alto livello (funzionali) che non è fatto dal compilatore C e, quindi, non può essere fatto in C. Stack walking è un altro esempio di qualcosa fatto al di sotto del livello di C da linguaggi di alto livello.
Jon Harrop,

2
1) Dovrei vedere il codice specifico per commentare, ma se gli allocatori / barriere scritti a mano nell'assemblatore sono più veloci, allora usa l'assemblatore scritto a mano. Non sono sicuro di cosa abbia a che fare con GC. 2) Dai un'occhiata qui: stackoverflow.com/a/9814654/441099 il punto non è se un linguaggio non GC può fare per te l'eliminazione della ricorsione della coda. Il punto è che puoi trasformare il tuo codice in modo che sia più veloce o più veloce. Se il compilatore di un linguaggio specifico può farlo automaticamente per te è una questione di convenienza. In un'astrazione abbastanza bassa puoi sempre farlo da solo se lo desideri.
Guy Sirton,

1
L'esempio di chiamata di coda in C funziona solo per il caso speciale di una funzione che si chiama da sola. C non può gestire il caso generale di funzioni che si chiamano a vicenda. Cadere verso l'assemblatore e presupporre un tempo infinito per lo sviluppo è un tarpit di Turing.
Jon Harrop,

3

È facile costruire una situazione artificiale in cui GC è infinitamente più efficiente dei metodi manuali: basta disporre che vi sia una sola "radice" per il garbage collector e che tutto sia spazzatura, quindi il passaggio GC è immediatamente completato.

Se ci pensate, questo è il modello utilizzato per la garbage collection della memoria allocata a un processo. Il processo muore, tutta la sua memoria è spazzatura, abbiamo finito. Anche in termini pratici, un processo che inizia, corre e muore senza lasciare traccia potrebbe essere più efficiente di uno che inizia e corre per sempre.

Per i programmi pratici, scritti in lingue con garbage collection, il vantaggio della garbage collection non è la velocità ma la correttezza e la semplicità.


Se è facile costruire un esempio artificiale, ti dispiacerebbe mostrarne uno semplice?
Mehrdad,

1
@Mehrdad Ha spiegato una semplice. Scrivi un programma in cui la versione GC non riesce a eseguire una garbage run prima di uscire. La versione gestita dalla memoria manuale sarà più lenta perché stava esplicitamente rintracciando e liberando cose.
btilly

3
@btilly: "Scrivi un programma in cui la versione GC non riesce a eseguire una garbage run prima di uscire." ... non riuscire a fare la garbage collection in primo luogo è una perdita di memoria dovuta alla mancanza di un GC funzionante, non un miglioramento delle prestazioni dovuto alla presenza di un GC! È come chiamare abort()in C ++ prima che il programma finisca. È un confronto insignificante; non stai nemmeno raccogliendo i rifiuti, stai solo lasciando perdere la memoria. Non si può dire che la raccolta dei rifiuti sia più veloce (o più lenta) se non si sta raccogliendo i rifiuti per cominciare ...
Mehrdad,

Per fare un esempio estremo, dovresti definire un sistema completo con la tua gestione di heap e heap, che sarebbe un grande progetto studentesco ma troppo grande per adattarsi a questo margine. Faresti abbastanza bene scrivendo un programma che alloca e distribuisce array di dimensioni casuali, in un modo progettato per essere stressante per i metodi di gestione della memoria non gc.
lugubre il

3
@Mehrdad Non è così. Lo scenario è che la versione GC non ha mai raggiunto la soglia alla quale avrebbe eseguito una corsa, non che non avrebbe funzionato correttamente su un set di dati diverso. Ciò sarà banalmente molto buono per la versione GC, anche se non è un buon predittore di eventuali prestazioni.
btilly

2

Va considerato che GC non è solo una strategia di gestione della memoria; richiede inoltre l'intera progettazione del linguaggio e dell'ambiente di runtime, che impongono costi (e benefici). Ad esempio, un linguaggio che supporta GC deve essere compilato in una forma in cui i puntatori non possono essere nascosti dal Garbage Collector e generalmente dove non possono essere costruiti se non con primitive di sistema gestite con cura. Un'altra considerazione è la difficoltà di mantenere le garanzie sui tempi di risposta, in quanto GC impone alcuni passaggi che devono essere eseguiti per il completamento.

Di conseguenza, se si dispone di una lingua che viene raccolta in modo inutile e si confronta la velocità con la memoria gestita manualmente nello stesso sistema, è comunque necessario pagare l'overhead per supportare la garbage collection anche se non la si utilizza.


2

Più veloce è dubbio. Tuttavia, può essere ultra-veloce, impercettibile o più veloce se è supportato dall'hardware. Ci sono stati progetti del genere per le macchine LISP molto tempo fa. Uno ha integrato il GC nel sottosistema di memoria dell'hardware in modo tale che la CPU principale non sapesse che era lì. Come molti progetti successivi, il GC funzionava contemporaneamente al processore principale con poche o nessuna necessità di pause. Un design più moderno sono le macchine Azul Systems Vega 3 che eseguono il codice Java molto più velocemente di quelle di JVM utilizzando processori appositamente progettati e un GC senza pause. Cercali su Google se vuoi sapere quanto GC (o Java) può essere veloce.


2

Ho lavorato parecchio su questo e ne ho descritto alcuni qui . Ho confrontato il GC Boehm in C ++, allocando usando mallocma non liberando, allocando e liberando usando freee un GC mark-region personalizzato scritto in C ++ tutto vs GC stock di OCaml che esegue un risolutore di n-queens basato su elenco. Il GC di OCaml è stato più veloce in tutti i casi. I programmi C ++ e OCaml sono stati scritti deliberatamente per eseguire le stesse allocazioni nello stesso ordine.

Ovviamente puoi riscrivere i programmi per risolvere il problema usando solo numeri interi a 64 bit e nessuna allocazione. Anche se più veloce questo avrebbe vanificato il punto dell'esercizio (che era di prevedere le prestazioni di un nuovo algoritmo GC, stavo lavorando su un prototipo incorporato in C ++).

Ho trascorso molti anni nel settore del porting di codice C ++ reale in linguaggi gestiti. In quasi ogni singolo caso ho osservato miglioramenti sostanziali delle prestazioni, molti dei quali probabilmente dovuti alla gestione manuale della memoria da parte di GC. La limitazione pratica non è ciò che può essere realizzato in un microbenchmark ma ciò che può essere realizzato prima di una scadenza e i linguaggi basati su GC offrono miglioramenti della produttività così enormi che non ho mai guardato indietro. Uso ancora C e C ++ su dispositivi embedded (microcontrollori) ma anche questo sta cambiando ora.


+1 grazie. Dove possiamo vedere ed eseguire il codice di riferimento?
Mehrdad,

Il codice è sparso sul posto. Ho pubblicato la versione mark-region qui: groups.google.com/d/msg/…
Jon Harrop,

1
Ci sono risultati sia per thread thread che non sicuri.
Jon Harrop,

1
@Mehrdad: "Hai eliminato tali potenziali fonti di errore?". Sì. OCaml ha un modello di compilazione molto semplice senza ottimizzazioni come l'analisi di escape. La rappresentazione della chiusura di OCaml è in realtà sostanzialmente più lenta della soluzione C ++, quindi dovrebbe davvero usare un'usanza List.filtercome fa il C ++. Ma sì, hai certamente ragione quando alcune operazioni RC possono essere eluse. Tuttavia, il problema più grande che vedo in natura è che le persone non hanno il tempo di eseguire tali ottimizzazioni a mano su grandi basi di codice industriale.
Jon Harrop,

2
Si assolutamente. Nessun ulteriore sforzo di scrittura, ma la scrittura del codice non è il collo di bottiglia con C ++. Il codice di mantenimento è. Mantenere il codice con questo tipo di complessità accidentale è un incubo. La maggior parte delle basi di codice industriali sono milioni di righe di codice. Semplicemente non devi avere a che fare con quello. Ho visto persone convertire tutto shared_ptrper correggere bug di concorrenza. Il codice è molto più lento ma, ehi, ora funziona.
Jon Harrop,

-1

Un tale esempio ha necessariamente un cattivo schema manuale di allocazione della memoria.

Assumi il miglior garbage collector GC. Dispone internamente di metodi per allocare memoria, determinare quale memoria può essere liberata e metodi per liberarla definitivamente. Insieme, questi richiedono meno tempo di tutti GC; un po 'di tempo è trascorso negli altri metodi di GC.

Consideriamo ora un allocatore manuale che utilizza lo stesso meccanismo di allocazione di GCe la cui free()chiamata mette da parte la memoria per essere liberata con lo stesso metodo di GC. Non ha una fase di scansione, né ha altri metodi. Richiede necessariamente meno tempo.


2
Un garbage collector può spesso liberare molti oggetti, senza dover mettere la memoria in uno stato utile dopo ognuno. Considerare il compito di rimuovere da un elenco di array tutti gli elementi che soddisfano un determinato criterio. La rimozione di un singolo elemento da un elenco di elementi N è O (N); rimuovendo M elementi da un elenco di N, uno alla volta è O (M * N). La rimozione di tutti gli elementi che soddisfano un criterio in un singolo passaggio nell'elenco, tuttavia, è O (1).
supercat

@supercat: freepuò anche raccogliere lotti. (E ovviamente rimuovere tutti gli elementi che soddisfano un criterio è ancora O (N), se non altro a causa della traversata della lista stessa)
MSalters

La rimozione di tutti gli elementi che soddisfano un criterio è almeno O (N). Hai ragione che freepotrebbe funzionare in una modalità di raccolta batch se ad ogni elemento di memoria fosse associato un flag, sebbene GC possa ancora emergere in alcune situazioni. Se si hanno riferimenti M che identificano L elementi distinti da un insieme di N cose, il tempo per rimuovere ogni riferimento a cui non esiste alcun riferimento e consolidare il resto è O (M) anziché O (N). Se uno ha M spazio extra disponibile, la costante di ridimensionamento può essere piuttosto piccola. Inoltre, la compactificazione in un sistema GC non a scansione richiede ...
supercat

@supercat: Beh, non è certamente O (1) come afferma la tua ultima frase nel primo commento.
Salterio

1
@MSalters: "E cosa impedirebbe a uno schema deterministico di avere un asilo nido?". Niente. Il raccoglitore di rifiuti di OCaml è deterministico e utilizza un vivaio. Ma non è "manuale" e penso che tu stia abusando della parola "deterministico".
Jon Harrop,
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.