Olá pessoal! Então, o longo fim de semana de março terminou. Queremos dedicar a primeira publicação pós-feriado ao amado por muitos cursos -
"Desenvolvedor Python" , que começa em menos de duas semanas. Vamos lá
Conteúdo- A memória é um livro vazio
- Gerenciamento de memória: do hardware ao software
- Implementação básica do Python
- Conceito Global Interpreter Lock (GIL)
- Coletor de lixo
- Gerenciamento de memória no CPython:
- Conclusão

Você já se perguntou como o Python nos bastidores processa seus dados? Como suas variáveis são armazenadas na memória? Em que ponto eles são removidos?
Neste artigo, vamos nos aprofundar na estrutura interna do Python para entender como o gerenciamento de memória funciona.
Depois de ler este artigo, você:
- Saiba mais sobre operações de baixo nível, especialmente memória.
- Entenda como o Python abstrai operações de baixo nível.
- Aprenda sobre os algoritmos de gerenciamento de memória no Python.
Conhecer a estrutura interna do Python fornecerá uma melhor compreensão dos princípios de seu comportamento. Espero que você possa dar uma olhada no Python de uma nova perspectiva. Nos bastidores, existem muitas operações lógicas para fazer seu programa funcionar corretamente.
A memória é um livro vazio.Você pode imaginar a memória do computador como um livro vazio, esperando que ele escreva muitos contos. Ainda não há nada em suas páginas, mas em breve aparecerão autores que desejam escrever suas histórias. Para fazer isso, eles precisarão de um lugar.
Como eles não podem escrever uma história em cima de outra, precisam ter muito cuidado com as páginas nas quais escrevem. Antes de começar a escrever, eles consultam o gerente do livro. O gerente decide onde, no livro, os autores podem escrever sua história.
Como o livro existe há muitos anos, muitas histórias ficam desatualizadas. Quando ninguém lê ou aborda a história, eles a removem para abrir caminho para novas histórias.
No fundo, a memória do computador é como um livro vazio. Blocos contínuos de memória de tamanho fixo são geralmente chamados de páginas, portanto essa analogia é útil.
Os autores podem ser vários aplicativos ou processos que precisam armazenar dados na memória. Um gerente que decide onde os autores podem escrever suas histórias desempenha o papel de um gerente de memória - um classificador. E quem apaga histórias antigas é um coletor de lixo.
Gerenciamento de memória: do hardware ao softwareGerenciamento de memória é o processo no qual os aplicativos de software lêem e gravam dados. O gerenciador de memória determina onde colocar os dados do programa. Como a quantidade de memória é obviamente, como o número de páginas do livro, o gerente precisa encontrar espaço livre para fornecer o uso pelo aplicativo. Esse processo é chamado de "alocação de memória".
Por outro lado, quando os dados não são mais necessários, eles podem ser excluídos. Nesse caso, eles falam sobre liberar memória. Mas do que é libertado e de onde vem?
Em algum lugar dentro do computador, existe um dispositivo físico que armazena dados quando você executa programas Python. O código Python passa por muitos níveis de abstração antes de chegar a este dispositivo.
Um dos principais níveis acima do equipamento (RAM, disco rígido etc.) é o sistema operacional. Ele gerencia solicitações de leitura e gravação na memória.
Acima do sistema operacional, há uma camada de aplicativo na qual existe uma das implementações do Python (conectada ao seu sistema operacional ou baixada do python.org). O gerenciamento de memória para código nessa linguagem de programação é regulado por ferramentas especiais do Python. Os algoritmos e estruturas que o Python usa para gerenciar a memória são o tópico principal deste artigo.
Implementação básica do PythonA implementação básica do Python, ou "Python puro", é CPython escrito em C.
Fiquei muito surpreso quando soube disso pela primeira vez. Como um idioma pode ser escrito em outro idioma ?! Bem, não literalmente, é claro, mas a ideia é algo assim.
A linguagem Python é descrita em um
manual de referência especial em inglês . No entanto, este guia por si só não é muito útil. Você ainda precisa de uma ferramenta para interpretar o código escrito pelas regras do diretório.
Você também precisará de algo para executar o código no seu computador. A implementação básica do Python fornece as duas condições. Ele converte o código Python em instruções executadas em uma máquina virtual.
Nota: As máquinas virtuais são semelhantes aos computadores físicos, mas estão incorporadas no software. Eles processam instruções básicas semelhantes ao código de montagem .
Python é uma linguagem de programação interpretada. Seu código Python é compilado usando instruções mais facilmente compreendidas pelo computador -
bytecode . Essas instruções são interpretadas pela máquina virtual quando você executa o código.
Você já viu arquivos com a extensão
.pyc ou a pasta
__pycache__ ? Este é o mesmo bytecode que é interpretado pela máquina virtual.
É importante entender que existem outras implementações além do CPython, por exemplo,
IronPython , que compila e executa no Microsoft Common Language Runtime (CLR).
O Jython compila no bytecode Java para executar em uma máquina virtual Java. Há também
PyPy sobre o qual você pode escrever um artigo separado, por isso vou mencionar apenas de passagem.
Neste artigo, focaremos no gerenciamento de memória usando ferramentas CPython.
Nota: As versões do Python são atualizadas e tudo pode acontecer no futuro. No momento da redação deste artigo, a versão mais recente era o
Python 3.7 .
Ok, temos o CPython escrito em C que interpreta o bytecode do Python. Como isso se relaciona com o gerenciamento de memória? Para começar, existem algoritmos e estruturas para gerenciar memória no código CPython, em C. Para entender esses princípios no Python, você precisa de um entendimento básico do CPython.
O CPython é escrito em C, que por sua vez não suporta programação orientada a objetos. Por esse motivo, o código CPython possui uma estrutura bastante interessante.
Você deve ter ouvido falar que tudo no Python é um objeto, mesmo tipos como int e str, por exemplo. Isso é verdade no nível de implementação do CPython. Existe uma estrutura chamada PyObject que todo objeto no CPython usa.
Nota: Uma estrutura em C é um tipo de dados definido pelo usuário que agrupa diferentes tipos de dados em si. Podemos desenhar uma analogia com linguagens orientadas a objetos e dizer que uma estrutura é uma classe com atributos, mas sem métodos.
PyObject é o progenitor de todos os objetos em Python, contendo apenas duas coisas:
- ob_refcnt : contador de referência;
- ob_type : ponteiro para outro tipo.
Um contador de referência é necessário para a coleta de lixo. Também temos um ponteiro para um tipo específico de objeto. Um tipo de objeto é apenas outra estrutura que descreve objetos em Python (como dict ou int).
Cada objeto possui um alocador de memória orientado a objetos que sabe como alocar memória e armazenar o objeto. Cada objeto também possui um liberador de recursos orientado a objetos que limpa a memória se seu conteúdo não for mais necessário.
Há um fator importante em falar sobre alocação de memória e sua limpeza. A memória é um recurso compartilhado de um computador, e uma coisa bastante desagradável pode acontecer se dois processos tentarem gravar dados no mesmo local de memória ao mesmo tempo.
Bloqueio Global de Interpretação (GIL)O GIL é uma solução para o problema geral de compartilhar memória entre recursos compartilhados, como a memória do computador. Quando dois encadeamentos tentam alterar o mesmo recurso ao mesmo tempo, eles pisam um no outro. Como resultado, uma confusão completa é formada na memória e nenhum processo finaliza seu trabalho com o resultado desejado.
Voltando à analogia com o livro, suponha que, dos dois autores, cada um decida que ele deve escrever sua história na página atual neste momento específico. Cada um deles ignora as tentativas do outro de escrever uma história e começa a escrever teimosamente na página. Como resultado, temos duas histórias, uma em cima da outra, e uma página absolutamente ilegível.
Uma das soluções para esse problema é precisamente o GIL, que bloqueia o intérprete enquanto o thread interage com o recurso alocado, permitindo assim que um e apenas um thread grave na área de memória alocada. Quando o CPython aloca memória, ele usa o GIL para garantir que esteja certo.
Essa abordagem tem muitas vantagens e muitas desvantagens; portanto, o GIL causa conflitos na comunidade Python. Para saber mais sobre o GIL, sugiro ler o seguinte
artigo .
Coletor de lixoVoltemos à nossa analogia com o livro e imaginemos que algumas histórias nele estão irremediavelmente desatualizadas. Ninguém os lê e os aborda. Nesse caso, uma solução natural seria se livrar deles como desnecessários, liberando espaço para novas histórias.
Tais histórias antigas não utilizadas podem ser comparadas com objetos no Python cuja contagem de referência caiu para 0. Lembre-se de que todo objeto no Python tem uma contagem de referência e um ponteiro para um tipo.
A contagem de referência pode aumentar por vários motivos. Por exemplo, aumentará se você atribuir uma variável a outra variável.

Também aumentará se você passar o objeto como argumento.

No último exemplo, a contagem de referência aumentará se você incluir o objeto na lista.

O Python permite que você saiba o valor atual do contador de referência usando o módulo sys. Você pode usar
sys.getrefcount(numbers)
, mas lembre-se de que chamar
getrefcount()
aumentará o contador de referência por outro.
De qualquer forma, se o objeto no seu código ainda for necessário, seu valor para o contador de referência será maior que 0. E quando cair para zero, uma função especial será iniciada para limpar a memória, que a liberará e disponibilizará para outros objetos.
Mas o que significa “liberar memória” e como outros objetos a usam? Vamos mergulhar diretamente no gerenciamento de memória no CPython.
Gerenciamento de memória no CPythonNesta parte, mergulharemos na arquitetura de memória CPython e nos algoritmos pelos quais ela opera.
Como mencionado anteriormente, existem níveis de abstração entre equipamentos físicos e CPython. O sistema operacional (SO) abstrai a memória física e cria um nível de memória virtual que aplicativos, incluindo Python, podem acessar.
Um gerenciador de memória virtual orientado ao sistema operacional aloca uma área de memória específica para processos Python. Na figura, as áreas cinza escuras são o espaço que o processo Python ocupa.

Python usa parte da memória para uso interno e memória não-objeto. A outra parte é dividida no armazenamento de objetos (seu
int, dict etc.). Agora eu falo em uma linguagem muito simples, mas você pode olhar embaixo do capô, ou seja, no
código fonte do CPython e ver como tudo isso acontece do ponto de vista prático. .
No CPython, há um alocador de objetos responsável por alocar memória dentro de uma área de memória de objetos. É neste distribuidor de objetos que toda a magia é realizada. É chamado sempre que cada novo objeto precisa ocupar ou liberar memória.
Geralmente, adicionar e remover dados no Python, como int ou list, por exemplo, não usa muitos dados ao mesmo tempo. É por isso que a arquitetura do dispensador se concentra em trabalhar com pequenas quantidades de dados por unidade de tempo. Além disso, ele não aloca memória antecipadamente, ou seja, até aquele momento até que se torne absolutamente necessário.
Os comentários no código-fonte definem o alocador como "um alocador rápido de memória para fins especiais que funciona como a função malloc universal". Assim, em C, o malloc é usado para alocar memória.
Agora, vamos dar uma olhada na estratégia de alocação de memória do CPython. Primeiro, vamos falar sobre as três partes principais e como elas se relacionam.
As arenas são as maiores áreas de memória que ocupam espaço até as bordas das páginas na memória. A borda da página (página espalhada) é o ponto extremo de um bloco contínuo de memória de comprimento fixo usado pelo sistema operacional. Python define a borda da página do sistema para 256 KB.

Dentro das arenas, existem pools (pool), que são considerados uma página virtual de memória (4 Kb). Eles parecem páginas em nossa analogia. Os pools são divididos em pedaços ainda menores de memória - blocos.
Todos os blocos no pool são encontrados em uma "classe de tamanho". A classe de tamanho determina o tamanho do bloco, com uma certa quantidade de dados solicitados. A graduação na tabela abaixo é obtida diretamente dos comentários no código-fonte:

Por exemplo, se forem necessários 42 bytes, os dados serão colocados em um bloco de 48 bytes.
PiscinasPiscinas são compostas de blocos da mesma classe de tamanho. Cada pool trabalha com o princípio de uma lista duplamente vinculada a outros pools da mesma classe de tamanho. Portanto, o algoritmo pode encontrar facilmente o local necessário para o tamanho de bloco necessário, mesmo entre muitos conjuntos.
A
usedpools list
controla todos os conjuntos que possuem algum tipo de espaço livre disponível para dados de cada classe de tamanho. Quando o tamanho do bloco necessário é solicitado, o algoritmo verifica a lista de conjuntos usados para encontrar um conjunto adequado para ele.
As piscinas estão em três estados: usadas, cheias, vazias. O pool usado contém blocos nos quais algumas informações podem ser gravadas. Os blocos do pool completo estão todos distribuídos e já contêm dados. Conjuntos vazios não contêm dados e podem ser divididos em quais classes de tamanho são adequadas, se necessário.
A lista de conjuntos vazios (lista de poços
freepools list
) contém, respectivamente, todos os conjuntos em um estado vazio. Mas em que ponto eles são usados?
Digamos que seu código precise de uma área de memória de 8 bytes. Se não houver conjuntos na lista de conjuntos usados com um tamanho de classe de 8 bytes, um novo conjunto vazio será inicializado como armazenamento de blocos de 8 bytes. Em seguida, o pool vazio é adicionado à lista de pools usados e pode ser usado nas seguintes consultas.
Um pool completo libera alguns blocos quando essas informações não são mais necessárias. Esse pool será adicionado à lista usada de acordo com sua classe de tamanho. Você pode observar como os conjuntos alteram seus estados e até mesmo classificam o tamanho de acordo com o algoritmo.
Blocos
Como pode ser visto na figura, os conjuntos contêm ponteiros para liberar blocos de memória. Há uma ligeira nuance em seu trabalho. De acordo com os comentários no código fonte, o distribuidor "se esforça para nunca tocar em nenhuma área de memória em nenhum dos níveis (arena, piscina, bloco) até que seja necessário".
Isso significa que um bloco pode ter três estados. Eles podem ser definidos da seguinte maneira:
- Intocado : áreas da memória que não foram alocadas;
- Livre : áreas de memória que foram alocadas, mas posteriormente liberadas pelo CPython porque não continham informações relevantes;
- Distribuído : áreas de memória que atualmente contêm informações atuais.
O ponteiro de bloco livre é uma lista vinculada única de blocos de memória livre. Em outras palavras, esta é uma lista de locais gratuitos onde você pode escrever informações. Se for necessária mais memória do que em blocos livres, o alocador usará os blocos intocados no pool.
Assim que o gerenciador de memória libera blocos, esses blocos são adicionados ao topo da lista de blocos livres. A lista real pode não conter uma sequência contínua de blocos de memória, como na primeira figura "bem-sucedida".
ArenasArenas contêm piscinas. As arenas, diferentemente dos pools, não possuem divisões de estado explícitas.
Eles mesmos são organizados em uma lista duplamente vinculada, chamada lista de arenas utilizáveis (usable_arenas). Essa lista é classificada pelo número de pools gratuitos. Quanto menos piscinas gratuitas, mais perto a arena do topo da lista.

Isso significa que a arena mais completa será selecionada para gravar ainda mais dados. Mas por que exatamente? Por que não gravar dados para onde está o espaço mais livre?
Isso nos leva à idéia de liberar completamente a memória. O fato é que, em alguns casos, quando a memória é liberada, ela permanece inacessível ao sistema operacional. O processo Python o mantém distribuído e o usa posteriormente para novos dados. A desalocação de memória cheia retorna a memória para o sistema operacional.
As arenas não são as únicas áreas que podem ser completamente desocupadas. Assim, entendemos que as arenas que estão na lista “mais perto do vazio” devem ser liberadas. Nesse caso, a área de memória pode realmente ser completamente liberada e, portanto, a capacidade total de memória do seu programa Python é reduzida.
ConclusãoO gerenciamento de memória é uma das partes mais importantes no trabalho com um computador. De uma forma ou de outra, o Python realiza praticamente todas as suas operações no modo furtivo.
Neste artigo você aprendeu:
- O que é gerenciamento de memória e por que é importante;
- O que é o CPython, uma implementação básica do Python;
- Como as estruturas e algoritmos de dados funcionam no gerenciamento de memória do CPython e armazenam seus dados.
O Python abstrai as muitas nuances de trabalhar com um computador. Isso torna possível trabalhar em um nível superior e se livrar da dor de cabeça no tópico de onde e como os bytes do seu programa são armazenados.
Então aprendemos sobre gerenciamento de memória em Python. Tradicionalmente, aguardamos seus comentários e também convidamos você para um
dia aberto no curso Python Developer, que ocorrerá em 13 de março