2013-03-16 15 views
94

Ho alcune domande relative all'utilizzo della memoria nel seguente esempio.Rilascio di memoria in Python

  1. Se corro nell'interprete,

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

    la vera memoria utilizzata sulla mia macchina va fino a 80.9mb. Ho poi,

    del foo 
    

    memoria reale va giù, ma solo per 30.4mb. L'interprete utilizza la base 4.4mb, quindi qual è il vantaggio nel non rilasciare 26mb di memoria nel sistema operativo? È perché Python sta "pianificando in anticipo", pensando di poter usare ancora quella memoria?

  2. Perché rilascia in particolare 50.5mb - qual è l'importo che viene rilasciato in base a?

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

+3

Vale la pena notare che questo comportamento non è specifico per Python. In genere, quando un processo libera una memoria allocata nell'heap, la memoria non viene rilasciata sul sistema operativo fino alla sua morte. – NPE

+0

La tua domanda richiede più cose, alcune delle quali sono dup, alcune delle quali sono inappropriate per SO, alcune delle quali potrebbero essere delle buone domande. Stai chiedendo se Python non rilascia la memoria, in quali circostanze è possibile/non può, qual è il meccanismo sottostante, perché è stato progettato in quel modo, se ci sono soluzioni alternative o qualcos'altro interamente? – abarnert

+2

@abarnert Ho combinato sottoquestioni simili. Per rispondere alle tue domande: so che Python rilascia qualche memoria nel sistema operativo, ma perché non tutto e perché la quantità che fa. Se ci sono circostanze in cui non può, perché? Che soluzioni alternative pure. – Jared

risposta

70

La memoria allocata nello heap può essere soggetta a segni di alta marea. Questo è complicato dall'ottimizzazione interna 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). Gli stessi pool si trovano in 256 arene KiB, quindi se viene utilizzato solo un blocco in un pool, l'intera arena KiB 256 non verrà rilasciata. In Python 3.3 l'allocatore di oggetti di piccole dimensioni è passato a utilizzare mappe di memoria anonime invece di heap, quindi dovrebbe funzionare meglio al rilascio della memoria.

Inoltre, i tipi predefiniti mantengono freelist di oggetti allocati in precedenza che potrebbero o meno utilizzare l'allocatore di oggetti di piccole dimensioni. Il tipo int mantiene un freelist con la propria memoria allocata e per la cancellazione è necessario chiamare PyInt_ClearFreeList(). Questo può essere chiamato indirettamente facendo un intero gc.collect.

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

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) 

uscita:

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

Edit:

sono passato a misurare rispetto alla dimensione VM processo per eliminare gli effetti di altri processi del sistema.

Il runtime C (ad esempio glibc, msvcrt) riduce l'heap quando lo spazio libero contiguo in alto raggiunge una soglia costante, dinamica o configurabile. Con glibc puoi accordarlo con mallopt (M_TRIM_THRESHOLD). Detto questo, non è sorprendente se l'heap si restringe di più - anche molto di più - rispetto al blocco che si è free.

In 3.x range non crea un elenco, quindi il test precedente non creerà 10 milioni di oggetti int. Anche se lo ha fatto, il tipo int in 3.x è fondamentalmente un 2.x long, che non implementa una lista freelance.

90

Sto indovinando la domanda che interessa davvero è:

C'è un modo per forzare Python per liberare tutta la memoria che è stata utilizzata (se sai che non si utilizza più di tanto memoria di nuovo)?

No, non c'è. Ma c'è una soluzione facile: i processi figli.

Se sono necessari 500 MB di memoria temporanea per 5 minuti, ma dopo di che è necessario eseguire altre 2 ore e non toccherà mai più quella memoria, generare un processo figlio per eseguire il lavoro a uso intensivo di memoria. Quando il processo figlio scompare, la memoria viene rilasciata.

Questo non è completamente banale e gratuito, ma è piuttosto semplice ed economico, che di solito è abbastanza buono per il commercio di cui valga la pena.

In primo luogo, il modo più semplice per creare un processo figlio è con concurrent.futures (o, per 3.1 e versioni precedenti, la futures backport su PyPI):

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

Se avete bisogno di un po 'più di controllo, utilizzare il multiprocessing modulo.

I costi sono:

  • avvio Processo è una specie di lenta su alcune piattaforme, in particolare di Windows. Qui stiamo parlando di millisecondi, non di minuti, e se stai facendo girare un bambino per fare un lavoro di 300 secondi, non te ne accorgi nemmeno. Ma non è gratuito.
  • Se la quantità di memoria temporanea che si utilizza in realtà è grande, ciò potrebbe causare lo swap del programma principale. Ovviamente stai risparmiando tempo a lungo termine, perché se quella memoria rimanesse in giro per sempre avrebbe dovuto portare a un certo punto lo scambio. Ma questo può trasformare la lentezza graduale in ritardi all-at-once (e precoci) molto evidenti in alcuni casi d'uso.
  • L'invio di grandi quantità di dati tra processi può essere lento. Di nuovo, se stai parlando di mandare oltre 2K di argomenti e di recuperare 64K di risultati, non te ne accorgi nemmeno, ma se stai inviando e ricevendo grandi quantità di dati, vorrai usare qualche altro meccanismo (un file, mmap o altro, le API della memoria condivisa in multiprocessing e così via).
  • L'invio di grandi quantità di dati tra processi significa che i dati devono essere selezionabili (oppure, se li si inserisce in un file o in una memoria condivisa, struct -able o idealmente ctypes -able).
+12

Chi ha downvoted, si preoccupa di spiegare perché? – abarnert

+0

Davvero un bel trucco, anche se non risolvendo il problema :(Ma mi piace molto – ddofborg

24

eryksun ha risposto alla domanda # 1, e io ho risposto alla domanda # 3 (l'originale # 4), ma ora cerchiamo di rispondere alla domanda # 2:

Perché si rilasciare 50.5mb in particolare - qual è l'importo che viene rilasciato in base a?

Quello su cui si basa è, in definitiva, tutta una serie di coincidenze all'interno di Python e malloc che sono molto difficili da prevedere.

Innanzitutto, a seconda di come si sta misurando la memoria, è possibile misurare solo le pagine effettivamente mappate in memoria.In tal caso, ogni volta che una pagina viene scambiata dal cercapersone, la memoria viene visualizzata come "liberata", anche se non è stata liberata.

Oppure si possono misurare pagine in uso, che possono o non possono contare pagine assegnate ma non toccate mai (su sistemi che ottimizzano eccessivamente l'allocazione, come linux), pagine allocate ma contrassegnate MADV_FREE, ecc.

Se si stanno veramente misurando le pagine allocate (che in realtà non è una cosa molto utile da fare, ma sembra essere quello che si sta chiedendo) e le pagine sono state effettivamente deallocate, due circostanze in cui ciò può Accade: O hai utilizzato brk o equivalente per ridurre il segmento di dati (molto raro oggigiorno), oppure hai utilizzato munmap o simile per rilasciare un segmento mappato. (C'è anche teoricamente una variante minore a questi ultimi, in quanto ci sono modi per liberare parte di un segmento-es mappato, rubare con MAP_FIXED per un MADV_FREE segmento che immediatamente unmap.)

Ma la maggior parte dei programmi non lo fanno allocare direttamente le cose dalle pagine di memoria; usano un allocatore stile malloc. Quando chiami free, l'allocatore può solo rilasciare pagine sul sistema operativo se ti capita di essere free l'ultimo oggetto live in una mappatura (o nelle ultime N pagine del segmento dati). Non c'è modo in cui la tua applicazione può ragionevolmente prevedere questo, o addirittura scoprire che è successo in anticipo.

CPython rende tutto ciò ancora più complicato: ha un allocatore di oggetti personalizzato a 2 livelli sopra un allocatore di memoria personalizzato sopra a malloc. (Vedi the source comments per una spiegazione più dettagliata). Inoltre, anche a livello di API C, molto meno Python, non si controlla nemmeno direttamente quando gli oggetti di livello superiore vengono deallocati.

Quindi, quando si rilascia un oggetto, come si fa a sapere se sta per rilasciare memoria sul sistema operativo? Bene, per prima cosa devi sapere che hai rilasciato l'ultimo riferimento (inclusi eventuali riferimenti interni che non conoscevi), consentendo al GC di deallocarlo. (A differenza di altre implementazioni, almeno CPython assegnerà un oggetto non appena è consentito.) Questo di solito rilascia almeno due cose al livello successivo (ad esempio, per una stringa, stai rilasciando l'oggetto PyString e la stringa buffer).

Se fai deallocare un oggetto, per sapere se questo fa sì che il livello successivo verso il basso per rilasciare un blocco di stoccaggio oggetto, è necessario conoscere lo stato interno del allocatore oggetto, e come è implementato. (E, ovviamente, non può accadere se non si sta deallocando l'ultima cosa che nel blocco, e anche allora, non può accadere.)

Se fai rilasciare un blocco di stoccaggio oggetto, per sapere se questo cause una chiamata free, è necessario conoscere lo stato interno dell'allocazione PyMem e il modo in cui è implementato. (Anche in questo caso, devi essere deallocando l'ultimo blocco in uso all'interno di una regione malloc ndr, e anche allora, non può accadere.)

Se faifree una regione malloc Ed, per sapere se questo cause un munmap o equivalente (o brk), è necessario conoscere lo stato interno di malloc e il modo in cui è implementato. E questo, a differenza degli altri, è altamente specifico per piattaforma. (E ancora, di solito devi deallocare l'ultimo in uso malloc all'interno di un segmento mmap, e anche allora, potrebbe non succedere.)

Quindi, se vuoi capire perché è successo a rilasciare esattamente 50.5 mb , dovrai rintracciarlo dal basso verso l'alto.Perché lo malloc ha cancellato 50,5 MB di pagine quando hai effettuato quelle o più chiamate free (probabilmente un po 'più di 50.5mb)? Dovresti leggere il numero malloc della tua piattaforma, quindi percorrere le varie tabelle ed elenchi per vedere il suo stato corrente. (Su alcune piattaforme, potrebbe persino fare uso di informazioni a livello di sistema, il che è praticamente impossibile da catturare senza creare un'istantanea del sistema per l'ispezione offline, ma fortunatamente questo non è di solito un problema.) E poi devi fai la stessa cosa ai 3 livelli sopra.

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

A meno che non si stia realizzando uno sviluppo a risorse limitate (ad esempio, incorporato), non si ha motivo di preoccuparsi di questi dettagli.

E se si è fare sviluppo limitato alle risorse, conoscere questi dettagli è inutile; devi praticamente eseguire un end-run su tutti questi livelli e in particolare su mmap la memoria di cui hai bisogno a livello di applicazione (possibilmente con un semplice, ben compreso, allocatore di zona specifico dell'applicazione in mezzo).

+0

Non sapevo di poter pubblicare più risposte .... – laike9m