Liberando memória em Python

128

Eu tenho algumas perguntas relacionadas ao uso de memória no exemplo a seguir.

  1. Se eu correr no intérprete,

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

    a memória real usada na minha máquina sobe 80.9mb. Eu então,

    del foo

    memória real diminui, mas apenas para 30.4mb. O intérprete usa a 4.4mblinha de base. Qual é a vantagem de não liberar 26mbmemória para o sistema operacional? É porque o Python está "planejando com antecedência", pensando que você pode usar tanta memória novamente?

  2. Por que ele é lançado 50.5mbem particular - qual é a quantidade lançada com base?

  3. Existe uma maneira de forçar o Python a liberar toda a memória usada (se você souber que não usará tanta memória novamente)?

NOTA Esta pergunta é diferente de Como posso liberar explicitamente memória no Python? porque essa pergunta lida principalmente com o aumento do uso de memória da linha de base, mesmo depois que o intérprete libera objetos por meio da coleta de lixo (com gc.collectou sem uso).

Jared
fonte
4
Vale ressaltar que esse comportamento não é específico do Python. Geralmente, quando um processo libera alguma memória alocada ao heap, a memória não é liberada de volta ao sistema operacional até que o processo morra.
NPE
Sua pergunta faz várias coisas - algumas são bobagens, algumas são inapropriadas para SO, outras podem ser boas perguntas. Você está perguntando se o Python não libera memória, exatamente em que circunstâncias ele pode / não pode, qual é o mecanismo subjacente, por que foi projetado dessa maneira, se há alguma solução alternativa ou algo completamente diferente?
abarnert
2
@abarnert Combinei subquestões que eram semelhantes. Para responder às suas perguntas: Eu sei que o Python libera um pouco de memória para o sistema operacional, mas por que não tudo isso e por que a quantidade que ele gera? Se há circunstâncias em que não pode, por quê? Quais soluções alternativas também.
Jared
@jww Acho que não. Essa questão realmente estava relacionada ao motivo pelo qual o processo do intérprete nunca liberou memória, mesmo depois de coletar completamente o lixo com chamadas para gc.collect.
Jared

Respostas:

86

A memória alocada no heap pode estar sujeita a marcas d'água alta. Isso é complicado pelas otimizações internas do Python para alocar objetos pequenos ( PyObject_Malloc) em conjuntos de 4 KiB, classificados para tamanhos de alocação em múltiplos de 8 bytes - até 256 bytes (512 bytes em 3.3). Os próprios pools estão em arenas de 256 KiB; portanto, se apenas um bloco em um pool for usado, toda a arena de 256 KiB não será liberada. No Python 3.3, o alocador de objetos pequenos foi alterado para o uso de mapas de memória anônima em vez do heap, portanto, ele deveria ter um desempenho melhor ao liberar memória.

Além disso, os tipos internos mantêm freelists de objetos alocados anteriormente que podem ou não usar o alocador de objetos pequenos. O inttipo mantém um freelist com sua própria memória alocada e limpá-lo requer que seja chamado PyInt_ClearFreeList(). Isso pode ser chamado indiretamente, fazendo um total gc.collect.

Tente assim e me diga o que você recebe. Aqui está o link para 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)

Resultado:

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

Editar:

Eu mudei para medir em relação ao tamanho da VM do processo para eliminar os efeitos de outros processos no sistema.

O tempo de execução C (por exemplo, glibc, msvcrt) reduz a pilha quando o espaço livre contíguo na parte superior atinge um limite constante, dinâmico ou configurável. Com glibc, você pode ajustar isso com mallopt(M_TRIM_THRESHOLD). Diante disso, não é de surpreender que o heap diminua mais - e até mais - do que o bloco que você free.

Na 3.x rangenão cria uma lista, portanto, o teste acima não cria 10 milhões de intobjetos. Mesmo assim, o inttipo em 3.x é basicamente um 2.x long, que não implementa um freelist.

Eryk Sun
fonte
Use memory_info()em vez de get_memory_info()e xé definido
Aziz Alto
Você recebe 10 ^ 7 ints mesmo no Python 3, mas cada um substitui a última variável do loop, para que nem todos existam de uma só vez.
Davis Herring
Eu encontrei um problema de vazamento de memória e acho que foi o que você respondeu aqui. Mas como posso provar meu palpite? Existe alguma ferramenta que possa mostrar que muitos pools estão alocados, mas apenas um pequeno bloco é usado?
ruiruige1991 31/01/19
130

Suponho que a pergunta com a qual você realmente se importa aqui é:

Existe uma maneira de forçar o Python a liberar toda a memória usada (se você souber que não usará tanta memória novamente)?

Não, não há. Mas há uma solução fácil: processos filho.

Se você precisar de 500 MB de armazenamento temporário por 5 minutos, mas depois disso precisará executar por mais 2 horas e não voltará a tocar em tanta memória, inicie um processo filho para fazer o trabalho intensivo em memória. Quando o processo filho desaparece, a memória é liberada.

Isso não é completamente trivial e gratuito, mas é bem fácil e barato, o que geralmente é bom o suficiente para que o comércio valha a pena.

Primeiro, a maneira mais fácil de criar um processo filho é com concurrent.futures(ou, para 3.1 e versões anteriores, o futuresbackport no PyPI):

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

Se você precisar de um pouco mais de controle, use o multiprocessingmódulo

Os custos são:

  • A inicialização do processo é meio lenta em algumas plataformas, principalmente no Windows. Estamos falando em milissegundos aqui, não em minutos, e se você estiver girando uma criança para fazer 300 segundos de trabalho, nem perceberá. Mas não é grátis.
  • Se a grande quantidade de memória temporária que você usa realmente for grande , isso poderá fazer com que seu programa principal seja trocado. É claro que você está economizando tempo a longo prazo, porque, se essa memória durar para sempre, teria que levar à troca em algum momento. Mas isso pode transformar a lentidão gradual em atrasos muito visíveis de uma vez (e cedo) em alguns casos de uso.
  • O envio de grandes quantidades de dados entre processos pode ser lento. Novamente, se você estiver falando sobre enviar mais de 2K de argumentos e recuperar 64K de resultados, nem perceberá, mas se estiver enviando e recebendo grandes quantidades de dados, poderá usar outro mecanismo (um arquivo, mmapped ou não; as APIs de memória compartilhada em multiprocessing; etc.).
  • O envio de grandes quantidades de dados entre processos significa que os dados precisam ser selecionáveis ​​(ou, se você os colar em um arquivo ou memória compartilhada, podem ser structativados ou idealmente ctypes).
abarnert
fonte
Muito bom truque, embora não resolvendo o problema :( Mas eu realmente gosto dele
ddofborg
32

eryksun respondeu à pergunta 1 e eu respondi à pergunta 3 (o original 4), mas agora vamos responder à pergunta 2:

Por que ele libera 50.5mb em particular - qual é a quantidade lançada com base?

O que se baseia é, finalmente, toda uma série de coincidências dentro do Python e mallocque são muito difíceis de prever.

Primeiro, dependendo de como você está medindo a memória, você pode estar medindo apenas páginas realmente mapeadas na memória. Nesse caso, sempre que uma página for trocada pelo pager, a memória aparecerá como "liberada", mesmo que não tenha sido liberada.

Ou você pode medir páginas em uso, que podem ou não contar páginas alocadas, mas nunca tocadas (em sistemas que otimizam demais a alocação, como linux), páginas alocadas, mas marcadas MADV_FREE, etc.

Se você realmente está avaliando as páginas alocadas (o que na verdade não é uma coisa muito útil, mas parece ser o que você está perguntando), e as páginas foram realmente desalocadas, há duas circunstâncias em que isso pode acontecer: você usou brkou equivalente para reduzir o segmento de dados (muito raro hoje em dia) ou você usoumunmap ou semelhante para liberar um segmento mapeado. (Também há, teoricamente, uma variante menor do último, pois há maneiras de liberar parte de um segmento mapeado - por exemplo, roube-o MAP_FIXEDpara um MADV_FREEsegmento que você imediatamente mapeia.)

Mas a maioria dos programas não aloca coisas diretamente das páginas da memória; eles usam ummalloc alocador de estilo. Quando você liga free, o alocador só pode liberar páginas para o sistema operacional se você estiver freeno último objeto ativo em um mapeamento (ou nas últimas N páginas do segmento de dados). Não há como seu aplicativo prever isso razoavelmente ou até detectar que isso aconteceu com antecedência.

O CPython torna isso ainda mais complicado - ele possui um alocador de objetos personalizado de dois níveis em cima de um alocador de memória personalizado malloc. (Vejo os comentários da fonte para obter uma explicação mais detalhada.) Além disso, mesmo no nível da API C, muito menos no Python, você nem controla diretamente quando os objetos de nível superior são desalocados.

Então, quando você libera um objeto, como você sabe se ele libera memória para o sistema operacional? Bem, primeiro você precisa saber que lançou a última referência (incluindo todas as referências internas desconhecidas), permitindo que o GC a desaloque. (Diferentemente de outras implementações, pelo menos o CPython desalocará um objeto assim que permitido.) Isso geralmente desaloca pelo menos duas coisas no próximo nível abaixo (por exemplo, para uma string, você está liberando o PyStringobjeto e o buffer da string )

Se vocês fazer desalocar um objeto, para saber se isso faz com que a próxima baixo nível para desalocar um bloco de armazenamento de objetos, você tem que saber o estado interno do objeto alocador, bem como a forma como ele é implementado. (Obviamente, isso não pode acontecer, a menos que você esteja desalocando a última coisa no bloco e, mesmo assim, pode não acontecer.)

Se você fazer desalocar um bloco de armazenamento de objetos, para saber se isso faz com que uma freechamada, você tem que saber o estado interno do alocador PyMem, bem como a forma como ele é implementado. (Novamente, você deve desalocar o último bloco em uso dentro de ummalloc região ed e, mesmo assim, isso pode não acontecer.)

Se você faz free uma mallocregião ed, para saber se isso causa um munmapou equivalente (ou brk), você precisa conhecer o estado interno do malloce também como ele é implementado. E este, diferentemente dos outros, é altamente específico da plataforma. (E, novamente, você geralmente precisa desalocar o último em uso mallocem um mmapsegmento e, mesmo assim, isso pode não acontecer.)

Portanto, se você quiser entender por que lançou exatamente 50,5mb, precisará rastrear de baixo para cima. Por que mallocdesmapear 50.5mb no valor de páginas quando você fez uma ou mais freechamadas (por provavelmente um pouco mais que 50.5mb)? Você precisaria ler a plataforma malloce percorrer as várias tabelas e listas para ver seu estado atual. (Em algumas plataformas, pode até fazer uso de informações no nível do sistema, o que é praticamente impossível de capturar sem fazer uma captura instantânea do sistema para inspecionar offline, mas felizmente isso geralmente não é um problema.) E então você precisa faça a mesma coisa nos 3 níveis acima disso.

Portanto, a única resposta útil para a pergunta é "Porque".

A menos que você esteja desenvolvendo recursos limitados (por exemplo, incorporados), não há motivos para se preocupar com esses detalhes.

E se você estiver desenvolvendo recursos limitados, conhecer esses detalhes é inútil; você praticamente precisa executar uma execução final em todos esses níveis e, especificamente, mmapna memória necessária no nível do aplicativo (possivelmente com um alocador de zona específico do aplicativo, simples e bem compreendido).

abarnert
fonte
2

Primeiro, você pode querer instalar olhares:

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

Em seguida, execute-o no terminal!

glances

No seu código Python, adicione no início do arquivo o seguinte:

import os
import gc # Garbage Collector

Depois de usar a variável "Big" (por exemplo: myBigVar) para a qual você gostaria de liberar memória, escreva no seu código python o seguinte:

del myBigVar
gc.collect()

Em outro terminal, execute seu código python e observe no terminal "glances" como a memória é gerenciada no seu sistema!

Boa sorte!

PS Presumo que você esteja trabalhando em um sistema Debian ou Ubuntu

de20ce
fonte