Algumas reflexões sobre este artigo .Outro dia, fiquei interessado em como o Python Memory Management funciona no CPython para Python3
para o Ubuntu de 64 bits .
Pouco de teoria
A biblioteca do sistema glibc possui um alocador de malloc. Cada processo possui uma área de memória chamada heap. Ao alocar memória dinamicamente chamando a função malloc, obtemos uma parte do monte desse processo. Se o tamanho da memória solicitada for pequeno (não mais que 128 KB), a memória poderá ser retirada das listas de pedaços livres. Se isso não for possível, a memória será
alocada usando a chamada de sistema mmap
(sbrk, brk) . A chamada do sistema mmap mapeia a memória virtual para a memória física. A memória é exibida em páginas de 4KB. Pedaços grandes (acima de 128 KB) são sempre alocados através da chamada do sistema mmap. Ao liberar memória, se um pequeno bloco livre estiver em uma área de memória descongelada, parte da memória poderá retornar ao sistema operacional. Pedaços grandes retornam imediatamente ao sistema operacional.
Informações extraídas de uma
palestra sobre alocadores em C.O CPython possui seu próprio alocador (PyMalloc) para o "heap privado" e alocadores para cada tipo de objeto que funciona "em cima" do primeiro. O PyMalloc solicita blocos de memória de 256 KB através do malloc na biblioteca do sistema operacional chamada Arenas. Eles, por sua vez, são divididos em Pools por 4KB. Cada Pool é dividido em pedaços de tamanho fixo e cada um pode ser dividido em pedaços de um de 64 tamanhos.
Alocadores para cada tipo usam os Chunks já alocados, se houver. Se não houver, o PyMalloc emitirá uma nova piscina da primeira arena, na qual há um local para uma nova piscina (as arenas são "classificadas" em ordem decrescente de ocupação). Se isso não funcionar, o PyMalloc solicita ao sistema operacional uma nova arena. Exceto quando o tamanho da memória solicitada é superior a 512B, a memória é alocada diretamente através do malloc da biblioteca do sistema.
Quando um objeto é excluído, a memória não é retornada ao sistema operacional, mas os Chunks simplesmente retornam aos Pools correspondentes e os Pools às Arenas. A arena retorna ao sistema operacional quando todos os Chunks são libertados. Pelo que parece, se um número relativamente pequeno de Chunks for usado na Arena, toda a memória da Arena será usada pelo PVM. Mas como os blocos com mais de 128 KB são alocados via mmap, a Arena gratuita retornará imediatamente ao sistema operacional.
Eu gostaria de focar em dois pontos:
- Acontece que o PyMalloc aloca 256 KB de memória física ao criar uma nova Arena.
- Somente Arenas gratuitas são retornadas ao sistema operacional.
Exemplo
Considere o seguinte exemplo:
iterations = 2000000 l = [] for i in range(iterations): l.append(None) for i in range(iterations): l[i] = {} s = []
No exemplo, é criada uma lista l de 2 milhões de elementos, todos apontando para um objeto Nenhum. No próximo ciclo, um objeto é criado para cada elemento - um dicionário vazio. Em seguida, é criada uma segunda lista s, cujos elementos apontam para alguns objetos referenciados por alguns elementos da primeira lista. Após o próximo rastreamento, os itens da lista l novamente começam a apontar para o objeto Nenhum. E no último ciclo, os dicionários são criados novamente para cada elemento da primeira lista.
Opções da lista S:
s = []
s = l[::2]
s = l[200000 // 2::]
s = l[::100]
Estamos interessados no consumo de memória em cada caso.
Executaremos este script com o log do PyMalloc ativado:
export PYTHONMALLOCSTATS="True" && python3 source.py 2>result.txt
Explicação dos resultados
Nas imagens, você pode ver o consumo de memória em cada caso.
No eixo da abscissa, não há correlação de valores com o momento em que esse consumo ocorreu, apenas cada valor nos logs está associado ao seu número de série.
"Sem elementos"
No primeiro caso, a lista s está vazia. Depois de criar os objetos no segundo ciclo, aproximadamente 500 MB de memória são consumidos. E todos esses objetos são excluídos no terceiro ciclo e a memória usada é retornada ao sistema operacional. No último ciclo, a memória dos objetos é alocada novamente, o que leva ao consumo dos mesmos 500 MB.
"A cada segundo"
No caso em que criamos uma lista com cada segundo elemento da lista l, podemos notar que a memória não é retornada ao sistema operacional. Ou seja, nesse caso, observamos uma situação em que dicionários de aproximadamente 250 MB são excluídos, mas em cada Pool existem Chunks que não são excluídos, pelo que as Arenas correspondentes não são liberadas. Porém, quando criamos os dicionários pela segunda vez, os Chunks gratuitos desses pools são reutilizados, e é por isso que apenas 250 MB de nova memória são alocados.
"A segunda metade"
No caso em que criamos uma lista a partir da segunda metade dos elementos da lista l, a primeira metade está em arenas separadas, devido às quais aproximadamente 250 MB de memória são retornados ao sistema operacional. Depois disso, aproximadamente 500 MB são realocados para novos dicionários, motivo pelo qual o consumo total na região é de 750 MB.
Nesse caso, diferentemente do segundo, a memória é parcialmente retornada ao sistema operacional. O que, por um lado, permite que outros processos usem essa memória, por outro lado, exige chamadas do sistema para liberá-la e realocá-la.
"Todo centésimo"
O último caso parece ser o mais interessante. Lá, criamos uma segunda lista de cada centésimo elemento da primeira lista, o que requer aproximadamente 5 MB. Porém, devido ao fato de um certo número de Chunks ocupados permanecer em cada Arena, essa memória não é liberada e o consumo permanece no nível de 500 MB. Quando criamos dicionários pela segunda vez, quase nenhuma memória nova é alocada e os Chunks alocados pela primeira vez são reutilizados.
Nesta situação, devido à fragmentação da memória, usamos 100 vezes mais do que precisamos. Porém, quando essa memória é necessária repetidamente, não precisamos fazer chamadas do sistema para alocá-la.
Sumário
Vale ressaltar que a fragmentação da memória é possível ao usar muitos alocadores. No entanto, você precisa usar cuidadosamente algumas estruturas de dados, por exemplo, aquelas que possuem uma estrutura em árvore, como as árvores de pesquisa. Como operações arbitrárias de adição e exclusão podem levar à situação descrita acima, pelo que a conveniência de usar essas estruturas em termos de consumo de memória estará em dúvida.
Código para renderizar imagens def parse_result(filename): ms = [] with open(filename, "r") as f: for line in f: if line.startswith("Total"): m = float(line.split()[-1].replace(",", "")) / 1024 / 1024 ms.append(m) return ms ms_1 = parse_result("_1.txt") ms_2 = parse_result("_2.txt") ms_3 = parse_result("_3.txt") ms_4 = parse_result("_4.txt") import matplotlib.pyplot as plt plt.figure(figsize=(20, 15)) fontdict = { "fontsize": 20, "fontweight" : 1, } plt.subplot(2, 2, 1) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_1) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 2) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_2) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 3) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_3) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 4) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_4) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15)