Rilascio di memoria in Python


128

Ho alcune domande correlate sull'utilizzo della memoria nel seguente esempio.

  1. Se corro nell'interprete,

    foo = ['bar' for _ in xrange(10000000)]

    la vera memoria utilizzata sulla mia macchina arriva fino a 80.9mb. Io poi,

    del foo

    la vera memoria si abbassa, ma solo per 30.4mb. L'interprete utilizza la 4.4mblinea di base, quindi qual è il vantaggio nel non rilasciare 26mbmemoria al sistema operativo? È perché Python sta "pianificando in anticipo", pensando che potresti usare di nuovo quella memoria?

  2. Perché viene rilasciato 50.5mbin particolare - su quale importo viene rilasciato in base?

  3. C'è un modo per forzare Python a rilasciare tutta la memoria che è stata utilizzata (se sai che non userai più quella memoria)?

NOTA Questa domanda è diversa da Come posso liberare esplicitamente la memoria in Python? perché questa domanda riguarda principalmente l'aumento dell'utilizzo della memoria dalla linea di base anche dopo che l'interprete ha liberato oggetti tramite garbage collection (con l'uso gc.collecto meno).


4
Vale la pena notare che questo comportamento non è specifico di Python. In genere, quando un processo libera parte della memoria allocata in heap, la memoria non viene rilasciata al sistema operativo fino a quando il processo non termina.
NPE,

La tua domanda pone più cose: alcune sono dups, altre inadatte alla SO, altre potrebbero essere buone domande. Stai chiedendo se Python non rilascia memoria, in quali circostanze può / non può esattamente, qual è il meccanismo sottostante, perché è stato progettato in questo modo, se ci sono soluzioni alternative o qualcos'altro?
Abarnert

2
@abarnert Ho combinato domande secondarie simili. Per rispondere alle tue domande: so che Python rilascia un po 'di memoria sul sistema operativo, ma perché non tutto e perché la quantità che fa. Se ci sono circostanze in cui non può, perché? Quali soluzioni alternative.
Jared


@jww Non la penso così. Questa domanda riguardava davvero il motivo per cui il processo dell'interprete non ha mai rilasciato la memoria anche dopo aver raccolto completamente la spazzatura con le chiamate a gc.collect.
Jared

Risposte:


86

La memoria allocata sull'heap può essere soggetta a segni di alta marea. Ciò è complicato dalle ottimizzazioni interne di Python per l'allocazione di piccoli oggetti ( PyObject_Malloc) in 4 pool KiB, classificati per dimensioni di allocazione a multipli di 8 byte - fino a 256 byte (512 byte in 3.3). I pool stessi si trovano in 256 arene KiB, quindi se viene utilizzato solo un blocco in un pool, l'intera arena da 256 KiB non verrà rilasciata. In Python 3.3 l'allocatore di oggetti di piccole dimensioni è stato convertito in mappe di memoria anonime anziché in heap, quindi dovrebbe funzionare meglio rilasciando memoria.

Inoltre, i tipi predefiniti mantengono liste di oggetti allocati in precedenza che possono o meno utilizzare l'allocatore di oggetti di piccole dimensioni. Il inttipo mantiene una lista libera con la propria memoria allocata e la sua cancellazione richiede la chiamata PyInt_ClearFreeList(). Questo può essere chiamato indirettamente facendo un pieno gc.collect.

Provalo in questo modo e dimmi cosa ottieni. Ecco il link per psutil.Process.memory_info .

import os
import gc
import psutil

proc = psutil.Process(os.getpid())
gc.collect()
mem0 = proc.get_memory_info().rss

# create approx. 10**7 int objects and pointers
foo = ['abc' for x in range(10**7)]
mem1 = proc.get_memory_info().rss

# unreference, including x == 9999999
del foo, x
mem2 = proc.get_memory_info().rss

# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.get_memory_info().rss

pd = lambda x2, x1: 100.0 * (x2 - x1) / mem0
print "Allocation: %0.2f%%" % pd(mem1, mem0)
print "Unreference: %0.2f%%" % pd(mem2, mem1)
print "Collect: %0.2f%%" % pd(mem3, mem2)
print "Overall: %0.2f%%" % pd(mem3, mem0)

Produzione:

Allocation: 3034.36%
Unreference: -752.39%
Collect: -2279.74%
Overall: 2.23%

Modificare:

Sono passato alla misurazione in relazione alla dimensione della VM del processo per eliminare gli effetti di altri processi nel sistema.

Il runtime C (ad es. Glibc, msvcrt) riduce l'heap quando lo spazio libero contiguo nella parte superiore raggiunge una soglia costante, dinamica o configurabile. Con glibc puoi sintonizzarlo con mallopt(M_TRIM_THRESHOLD). Detto questo, non sorprende se l'heap si restringe di più - anche molto di più - rispetto al blocco che tu free.

In 3.x rangenon viene creato un elenco, quindi il test sopra riportato non crea 10 milioni di intoggetti. Anche se lo ha fatto, il inttipo in 3.x è fondamentalmente un 2.x long, che non implementa un freelist.


Usa memory_info()invece di get_memory_info()ed xè definito
Aziz Alto,

Ottieni 10 ^ 7 ints anche in Python 3, ma ognuno sostituisce l'ultimo nella variabile loop in modo che non esistano tutti in una volta.
Davis Herring,

Ho riscontrato un problema di perdita di memoria e immagino il motivo per cui hai risposto qui. Ma come posso provare la mia ipotesi? Esiste uno strumento in grado di mostrare che molti pool sono malloced, ma viene utilizzato solo un piccolo blocco?
ruiruige1991

130

Immagino che la domanda a cui tieni davvero qui sia:

C'è un modo per forzare Python a rilasciare tutta la memoria che è stata utilizzata (se sai che non userai più quella memoria)?

No non c'è. Ma esiste una soluzione semplice: i processi figlio.

Se hai bisogno di 500 MB di spazio di archiviazione temporaneo per 5 minuti, ma dopo di ciò devi correre per altre 2 ore e non toccherai più quella memoria, genera un processo figlio per eseguire il lavoro ad alta intensità di memoria. Quando il processo figlio scompare, la memoria viene rilasciata.

Questo non è del tutto banale e gratuito, ma è abbastanza facile ed economico, che di solito è abbastanza buono per il commercio per essere utile.

Innanzitutto, il modo più semplice per creare un processo figlio è con concurrent.futures(o, per 3.1 e precedenti, il futuresbackport su PyPI):

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
    result = executor.submit(func, *args, **kwargs).result()

Se hai bisogno di un po 'più di controllo, usa il multiprocessingmodulo.

I costi sono:

  • L'avvio del processo è piuttosto lento su alcune piattaforme, in particolare Windows. Stiamo parlando di millisecondi qui, non di minuti, e se fai girare un bambino per fare un lavoro di 300 secondi, non te ne accorgerai nemmeno. Ma non è gratuito.
  • Se la grande quantità di memoria temporanea che usi è davvero grande , farlo può far sì che il tuo programma principale venga scambiato. Ovviamente stai risparmiando tempo a lungo termine, perché se quel ricordo restasse in sospeso per sempre, a un certo punto dovrebbe portare allo scambio. Ma questo può trasformare la lentezza graduale in ritardi all-in-one (e precoci) molto evidenti in alcuni casi d'uso.
  • L'invio di grandi quantità di dati tra processi può essere lento. Ancora una volta, se stai parlando di inviare oltre 2K di argomenti e di recuperare 64K di risultati, non te ne accorgerai nemmeno, ma se stai inviando e ricevendo grandi quantità di dati, ti consigliamo di utilizzare qualche altro meccanismo (un file, mmapped o altro; le API della memoria condivisa in multiprocessing; ecc.).
  • L'invio di grandi quantità di dati tra processi significa che i dati devono essere selezionabili (o, se li si inserisce in un file o memoria condivisa, structabilitabili o idealmente ctypes).

Davvero un bel trucco, anche se non risolve il problema :( Ma mi piace davvero
ddofborg

32

eryksun ha risposto alla domanda n. 1 e ho risposto alla domanda n. 3 (l'originale n. 4), ma ora rispondiamo alla domanda n. 2:

Perché in particolare rilascia 50.5mb - su quale importo viene rilasciato in base?

Ciò su cui si basa è, in definitiva, un'intera serie di coincidenze all'interno di Python e mallocche sono molto difficili da prevedere.

Innanzitutto, a seconda di come stai misurando la memoria, potresti misurare solo le pagine effettivamente mappate nella memoria. In tal caso, ogni volta che una pagina viene sostituita dal cercapersone, la memoria verrà visualizzata come "liberata", anche se non è stata liberata.

Oppure potresti misurare pagine in uso, che possono o meno contare pagine allocate ma mai toccate (su sistemi che allocano in modo ottimistico, come Linux), pagine allocate ma taggate MADV_FREE, ecc.

Se stai davvero misurando le pagine allocate (che in realtà non è una cosa molto utile da fare, ma sembra essere ciò di cui stai chiedendo), e le pagine sono state davvero dislocate, due circostanze in cui ciò può accadere: O tu ' brkhai usato o equivalente per ridurre il segmento di dati (molto raro al giorno d'oggi), oppure hai usato munmapo simile per rilasciare un segmento mappato. (In teoria esiste anche una variante minore di quest'ultima, in quanto vi sono modi per rilasciare parte di un segmento mappato, ad esempio rubandolo con MAP_FIXEDun MADV_FREEsegmento che si annulla immediatamente.)

Ma la maggior parte dei programmi non alloca direttamente le cose dalle pagine di memoria; usano un mallocallocatore di stile. Quando chiami free, l'allocatore può rilasciare pagine sul sistema operativo solo se ti capita di essere freel'ultimo oggetto attivo in una mappatura (o nelle ultime N pagine del segmento di dati). Non è possibile che l'applicazione possa ragionevolmente prevederlo o persino rilevarlo in anticipo.

CPython lo rende ancora più complicato: ha un allocatore di oggetti a 2 livelli personalizzato sopra un allocatore di memoria personalizzato malloc. (Vedi i commenti di origine per una spiegazione più dettagliata.) E per di più, anche a livello di API C, molto meno Python, non controlli nemmeno direttamente quando gli oggetti di livello superiore sono deallocati.

Quindi, quando rilasci un oggetto, come fai a sapere se rilascerà memoria sul sistema operativo? Bene, prima devi sapere che hai rilasciato l'ultimo riferimento (compresi eventuali riferimenti interni di cui non sapevi), consentendo al GC di deallocare. (A differenza di altre implementazioni, almeno CPython dislocerà un oggetto non appena è permesso.) Questo di solito disloca almeno due cose al livello successivo verso il basso (ad esempio, per una stringa, stai rilasciando l' PyStringoggetto e il buffer delle stringhe ).

Se si effettua la deallocazione di un oggetto, per sapere se ciò provoca il deallocazione di un blocco di archiviazione di oggetti al livello successivo, è necessario conoscere lo stato interno dell'allocatore di oggetti e come viene implementato. (Ovviamente non può succedere a meno che non si stia deallocando l'ultima cosa nel blocco e anche in questo caso, potrebbe non accadere.)

Se si fa rilasciare un blocco di stoccaggio oggetto, per sapere se questo provoca una freechiamata, è necessario conoscere lo stato interno del allocatore PyMem, e come è implementato. (Ancora una volta, devi deallocare l'ultimo blocco in uso all'interno di una mallocregione ed, e anche in questo caso, potrebbe non accadere.)

Se si esegue free una mallocregione ed, per sapere se ciò provoca un munmapo equivalente (o brk), è necessario conoscere lo stato interno del malloc, nonché come è implementato. E questo, a differenza degli altri, è altamente specifico per la piattaforma. (E di nuovo, in genere è necessario deallocare l'ultimo in uso mallocall'interno di un mmapsegmento e anche in questo caso, potrebbe non accadere.)

Quindi, se vuoi capire perché è successo a rilasciare esattamente 50.5mb, dovrai rintracciarlo dal basso verso l'alto. Perché non mallocmappare 50.5 MB di pagine quando hai fatto quelle una o più freechiamate (probabilmente per un po 'più di 50.5 MB)? Dovresti leggere la tua piattaforma malloc, quindi camminare tra le varie tabelle ed elenchi per vedere il suo stato attuale. (Su alcune piattaforme, potrebbe persino fare uso di informazioni a livello di sistema, che è praticamente impossibile da catturare senza fare un'istantanea del sistema per ispezionare offline, ma per fortuna questo di solito non è un problema.) E poi devi fai la stessa cosa ai 3 livelli sopra.

Quindi, l'unica risposta utile alla domanda è "Perché".

A meno che tu non stia facendo uno sviluppo limitato di risorse (ad esempio, incorporato), non hai motivo di preoccuparti di questi dettagli.

E se stai sviluppando risorse limitate, conoscere questi dettagli è inutile; devi praticamente eseguire un end-run attorno a tutti quei livelli e in particolare mmapalla memoria di cui hai bisogno a livello dell'applicazione (possibilmente con un allocatore di zona semplice, ben compreso e specifico per l'applicazione in mezzo).


2

Innanzitutto, potresti voler installare gli sguardi:

sudo apt-get install python-pip build-essential python-dev lm-sensors 
sudo pip install psutil logutils bottle batinfo https://bitbucket.org/gleb_zhulik/py3sensors/get/tip.tar.gz zeroconf netifaces pymdstat influxdb elasticsearch potsdb statsd pystache docker-py pysnmp pika py-cpuinfo bernhard
sudo pip install glances

Quindi eseguilo nel terminale!

glances

Nel tuo codice Python, aggiungi all'inizio del file, il seguente:

import os
import gc # Garbage Collector

Dopo aver usato la variabile "Big" (ad esempio: myBigVar) per la quale, desideri liberare memoria, scrivi nel tuo codice Python quanto segue:

del myBigVar
gc.collect()

In un altro terminale, esegui il tuo codice Python e osserva nel terminale "glances" come viene gestita la memoria nel tuo sistema!

In bocca al lupo!

PS Presumo che tu stia lavorando su un sistema Debian o Ubuntu

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.