Gestion de la mémoire Python

Bonjour à tous! La longue fin de semaine de mars s'est donc terminée. Nous voulons dédier la première publication post-vacances aux bien-aimés de nombreux cours - "Développeur Python" , qui commence dans moins de 2 semaines. Allons-y.

Table des matières

  1. La mémoire est un livre vide
  2. Gestion de la mémoire: du matériel au logiciel
  3. Implémentation de la base Python
  4. Concept Global Interpreter Lock (GIL)
  5. Collecteur d'ordures
  6. Gestion de la mémoire dans CPython:
    • Piscines
    • Blocs
    • Arènes
  7. Conclusion



Vous êtes-vous déjà demandé comment Python backstage traite vos données? Comment vos variables sont-elles stockées en mémoire? À quel moment sont-ils supprimés?
Dans cet article, nous allons approfondir la structure interne de Python pour comprendre comment fonctionne la gestion de la mémoire.

Après avoir lu cet article, vous:

  • En savoir plus sur les opérations de bas niveau, en particulier la mémoire.
  • Comprenez comment Python résume les opérations de bas niveau.
  • Découvrez les algorithmes de gestion de la mémoire en Python.

Connaître la structure interne de Python permettra de mieux comprendre les principes de son comportement. J'espère que vous pourrez jeter un œil à Python sous un nouvel angle. Dans les coulisses, il y a tellement d'opérations logiques pour faire fonctionner correctement votre programme.

La mémoire est un livre vide

Vous pouvez imaginer la mémoire de l'ordinateur comme un livre vide, en attendant qu'il écrive beaucoup d'histoires courtes. Il n'y a encore rien sur ses pages, mais des auteurs apparaîtront bientôt qui souhaitent y écrire leurs histoires. Pour ce faire, ils auront besoin d'une place.
Puisqu'ils ne peuvent pas écrire une histoire au-dessus d'une autre, ils doivent faire très attention aux pages sur lesquelles ils écrivent. Avant de commencer à écrire, ils consultent le gestionnaire de livres. Le manager décide où dans le livre les auteurs peuvent écrire leur histoire.

Depuis que le livre existe depuis de nombreuses années, de nombreuses histoires y sont dépassées. Lorsque personne ne lit ou ne traite de l'histoire, il la supprime pour faire place à de nouvelles histoires.
Au fond, la mémoire de l'ordinateur est comme un livre vide. Les blocs de mémoire continus d'une longueur fixe sont généralement appelés pages, donc cette analogie est très pratique.

Les auteurs peuvent être diverses applications ou processus qui doivent stocker des données en mémoire. Un gestionnaire qui décide où les auteurs peuvent écrire leurs histoires joue le rôle d'un gestionnaire de mémoire - un trieur. Et celui qui efface les vieilles histoires est un ramasse-miettes.

Gestion de la mémoire: du matériel au logiciel

La gestion de la mémoire est le processus dans lequel les applications logicielles lisent et écrivent des données. Le gestionnaire de mémoire détermine où placer les données du programme. Étant donné que la quantité de mémoire est bien sûr, comme le nombre de pages dans le livre, en conséquence, le gestionnaire doit trouver de l'espace libre pour le fournir pour une utilisation par l'application. Ce processus est appelé «allocation de mémoire».

En revanche, lorsque les données ne sont plus nécessaires, elles peuvent être supprimées. Dans ce cas, ils parlent de libérer de la mémoire. Mais de quoi est-il libéré et d'où vient-il?
Quelque part à l'intérieur de l'ordinateur, il y a un périphérique physique qui stocke les données lorsque vous exécutez des programmes Python. Le code Python passe par de nombreux niveaux d'abstraction avant d'accéder à cet appareil.

L'un des principaux niveaux situés au-dessus de l'équipement (RAM, disque dur, etc.) est le système d'exploitation. Il gère les demandes de lecture et d'écriture en mémoire.
Au-dessus du système d'exploitation, il y a une couche d'application dans laquelle se trouve l'une des implémentations Python (câblée dans votre système d'exploitation ou téléchargée depuis python.org). La gestion de la mémoire pour le code dans ce langage de programmation est régulée par des outils spéciaux Python. Les algorithmes et les structures que Python utilise pour gérer la mémoire sont le sujet principal de cet article.

Implémentation de la base Python

L'implémentation de base de Python, ou «pur Python», est CPython écrit en C.
J'ai été très surpris lorsque j'en ai entendu parler pour la première fois. Comment une langue peut-elle être écrite dans une autre langue?! Eh bien, pas littéralement, bien sûr, mais l'idée est quelque chose comme ça.

Le langage Python est décrit dans un manuel de référence spécial en anglais . Cependant, ce guide à lui seul n'est pas très utile. Vous avez toujours besoin d'un outil pour interpréter le code écrit par les règles du répertoire.

Vous aurez également besoin de quelque chose pour exécuter le code sur votre ordinateur. L'implémentation de base de Python fournit les deux conditions. Il convertit le code Python en instructions qui sont exécutées dans une machine virtuelle.

Remarque: les machines virtuelles sont similaires aux ordinateurs physiques, mais elles sont intégrées au logiciel. Ils traitent des instructions de base similaires au code assembleur .


Python est un langage de programmation interprété. Votre code Python est compilé en utilisant des instructions plus facilement comprises par l'ordinateur - bytecode . Ces instructions sont interprétées par la machine virtuelle lorsque vous exécutez le code.

Avez-vous déjà vu des fichiers avec l'extension .pyc ou le dossier __pycache__ ? Il s'agit du même bytecode qui est interprété par la machine virtuelle.
Il est important de comprendre qu'il existe d'autres implémentations en plus de CPython, par exemple IronPython , qui compile et s'exécute dans Microsoft Common Language Runtime (CLR). Jython se compile en bytecode Java pour s'exécuter sur une machine virtuelle Java. Il y a aussi PyPy sur lequel vous pouvez écrire un article séparé, donc je ne le mentionnerai qu'en passant.

Dans cet article, nous nous concentrerons sur la gestion de la mémoire à l'aide des outils CPython.
Remarque: les versions de Python sont mises à jour et tout peut arriver à l'avenir. Au moment d'écrire ces lignes, la dernière version était Python 3.7 .

Ok, nous avons CPython écrit en C qui interprète le bytecode Python. Quel est le lien avec la gestion de la mémoire? Pour commencer, les algorithmes et les structures de gestion de la mémoire existent dans le code CPython, en C. Pour comprendre ces principes en Python, vous avez besoin d'une compréhension de base de CPython.

CPython est écrit en C, qui à son tour ne prend pas en charge la programmation orientée objet. Pour cette raison, le code CPython a une structure plutôt intéressante.

Vous devez avoir entendu que tout en Python est un objet, même des types comme int et str, par exemple. Cela est vrai au niveau de l'implémentation de CPython. Il existe une structure appelée PyObject que chaque objet de CPython utilise.

Remarque: Une structure en C est un type de données défini par l'utilisateur qui regroupe différents types de données en soi. Nous pouvons faire une analogie avec les langages orientés objet et dire qu'une structure est une classe avec des attributs, mais sans méthodes.

PyObject est l'ancêtre de tous les objets en Python, contenant seulement deux choses:

  • ob_refcnt : compteur de référence;
  • ob_type : pointeur vers un autre type.

Un compteur de référence est requis pour la récupération de place. Nous avons également un pointeur sur un type d'objet spécifique. Un type d'objet est juste une autre structure qui décrit des objets en Python (tels que dict ou int).

Chaque objet a un allocateur de mémoire orienté objet qui sait allouer de la mémoire et stocker l'objet. Chaque objet possède également un libérateur de ressources orienté objet qui nettoie la mémoire si son contenu n'est plus nécessaire.

Il y a un facteur important lorsque l'on parle d'allocation de mémoire et de son nettoyage. La mémoire est une ressource partagée d'un ordinateur et une chose plutôt désagréable peut se produire si deux processus tentent d'écrire des données dans le même emplacement mémoire en même temps.

Verrou d'interprétation globale (GIL)

GIL est une solution au problème général du partage de mémoire entre des ressources partagées telles que la mémoire d'un ordinateur. Lorsque deux threads tentent de changer la même ressource en même temps, ils marchent sur les talons l'un de l'autre. En conséquence, un désordre complet est formé dans la mémoire et aucun processus ne terminera son travail avec le résultat souhaité.

Revenant à l'analogie avec le livre, supposons que des deux auteurs, chacun décide qu'il doit écrire son histoire sur la page en cours à ce moment particulier. Chacun d'eux ignore les tentatives de l'autre pour écrire une histoire et commence à écrire obstinément sur la page. En conséquence, nous avons deux histoires, l'une au-dessus de l'autre, et une page absolument illisible.

Une des solutions à ce problème est précisément GIL, qui bloque l'interpréteur pendant que le thread interagit avec la ressource allouée, permettant ainsi à un et un seul thread d'écrire dans la zone de mémoire allouée. Lorsque CPython alloue de la mémoire, il utilise le GIL pour s'assurer qu'il le fait correctement.
Cette approche présente à la fois de nombreux avantages et de nombreux inconvénients, c'est pourquoi GIL provoque des conflits dans la communauté Python. Pour en savoir plus sur GIL, je vous suggère de lire l' article suivant.

Collecteur d'ordures

Revenons à notre analogie avec le livre et imaginons que certaines histoires qu'il contient sont désespérément dépassées. Personne ne les lit et ne s’adresse à eux. Dans ce cas, une solution naturelle serait de s'en débarrasser comme inutile, libérant ainsi de l'espace pour de nouvelles histoires.
Ces vieilles histoires inutilisées peuvent être comparées à des objets en Python dont le nombre de références est tombé à 0. N'oubliez pas que chaque objet en Python a un nombre de références et un pointeur vers un type.

Le nombre de références peut augmenter pour plusieurs raisons. Par exemple, il augmentera si vous affectez une variable à une autre variable.



Il augmentera également si vous passez l'objet en argument.



Dans le dernier exemple, le nombre de références augmentera si vous incluez l'objet dans la liste.



Python vous permet de connaître la valeur actuelle du compteur de référence à l'aide du module sys. Vous pouvez utiliser sys.getrefcount(numbers) , mais n'oubliez pas que l'appel à getrefcount() incrémentera le compteur de référence d'un autre.

Dans tous les cas, si l'objet de votre code est toujours nécessaire, sa valeur pour son compteur de référence sera supérieure à 0. Et lorsqu'il tombe à zéro, une fonction spéciale sera lancée pour effacer la mémoire, ce qui la libérera et la rendra disponible pour d'autres objets.

Mais que signifie «libérer de la mémoire» et comment les autres objets l'utilisent-ils? Plongeons directement dans la gestion de la mémoire dans CPython.

Gestion de la mémoire dans CPython

Dans cette partie, nous allons plonger dans l'architecture de mémoire CPython et les algorithmes par lesquels elle fonctionne.

Comme mentionné précédemment, il existe des niveaux d'abstraction entre l'équipement physique et CPython. Le système d'exploitation (OS) extrait la mémoire physique et crée un niveau de mémoire virtuelle auquel les applications, y compris Python, peuvent accéder.

Un gestionnaire de mémoire virtuelle orienté OS alloue une zone mémoire spécifique aux processus Python. Dans l'image, les zones gris foncé sont l'espace occupé par le processus Python.



Python utilise une partie de la mémoire pour une utilisation interne et une mémoire non objet. L'autre partie est divisée en stockage d'objets (votre int, dict , etc.) Maintenant, je parle dans un langage très simple, mais vous pouvez regarder directement sous le capot, c'est-à-dire dans le code source de CPython et voir comment tout cela se passe d'un point de vue pratique .

Dans CPython, il existe un allocateur d'objets responsable de l'allocation de mémoire dans une zone de mémoire d'objets. C'est dans ce distributeur d'objets que se réalise toute magie. Il est appelé à chaque fois que chaque nouvel objet doit occuper ou libérer de la mémoire.

Habituellement, l'ajout et la suppression de données en Python, comme int ou list, par exemple, n'utilisent pas beaucoup de données à la fois. C'est pourquoi l'architecture du distributeur se concentre sur le travail avec de petites quantités de données par unité de temps. De plus, il n'alloue pas de mémoire à l'avance, c'est-à-dire jusqu'à ce moment jusqu'à ce qu'elle devienne absolument nécessaire.

Les commentaires dans le code source définissent l'allocateur comme «un allocateur de mémoire rapide à usage spécial qui fonctionne comme la fonction malloc universelle». Par conséquent, en C, malloc est utilisé pour allouer de la mémoire.

Voyons maintenant la stratégie d'allocation de mémoire de CPython. Tout d'abord, parlons des trois parties principales et de leur relation.

Les arènes sont les plus grandes zones de mémoire qui occupent l'espace jusqu'aux frontières des pages en mémoire. La bordure de page (page spread) est le point extrême d'un bloc de mémoire continu d'une longueur fixe utilisé par l'OS. Python définit la bordure de la page système sur 256 Ko.



À l'intérieur des arènes se trouvent des pools (pool), qui sont considérés comme une page virtuelle de mémoire (4 Ko). Ils ressemblent à des pages dans notre analogie. Les pools sont divisés en blocs de mémoire encore plus petits.

Tous les blocs du pool se trouvent dans une «classe de taille». La classe de taille détermine la taille du bloc, ayant une certaine quantité de données demandées. La gradation dans le tableau ci-dessous est tirée directement des commentaires dans le code source:



Par exemple, si 42 octets sont nécessaires, les données seront placées dans un bloc de 48 octets.

Piscines

Les piscines sont constituées de blocs de la même classe de taille. Chaque pool fonctionne sur le principe d'une liste doublement liée avec d'autres pools de la même classe de taille. Par conséquent, l'algorithme peut facilement trouver la place nécessaire pour la taille de bloc requise, même parmi de nombreux pools.

La usedpools list conserve une trace de tous les pools qui ont une sorte d'espace libre disponible pour les données de chaque classe de taille. Lorsque la taille de bloc requise est demandée, l'algorithme vérifie la liste des pools utilisés pour trouver un pool approprié pour celui-ci.

Les pools sont dans trois états: utilisé, plein, vide. Le pool utilisé contient des blocs dans lesquels certaines informations peuvent être écrites. Les blocs du pool complet sont tous distribués et contiennent déjà des données. Les pools vides ne contiennent aucune donnée et peuvent être décomposés en quelles classes de taille conviennent si nécessaire.

La liste des pools vides ( freepools list ) contient, respectivement, tous les pools dans un état vide. Mais à quel moment sont-ils utilisés?

Disons que votre code a besoin d'une zone mémoire de 8 octets. S'il n'y a pas de pools dans la liste des pools utilisés avec une taille de classe de 8 octets, un nouveau pool vide est initialisé comme stockant des blocs de 8 octets. Ensuite, le pool vide est ajouté à la liste des pools utilisés et peut être utilisé dans les requêtes suivantes.

Un pool complet libère certains blocs lorsque ces informations ne sont plus nécessaires. Ce pool sera ajouté à la liste utilisée selon sa classe de taille. Vous pouvez observer comment les pools changent leurs états et même leurs classes de taille en fonction de l'algorithme.

Blocs



Comme le montre la figure, les pools contiennent des pointeurs pour libérer des blocs de mémoire. Il y a une légère nuance dans leur travail. Selon les commentaires du code source, le distributeur "s'efforce de ne jamais toucher à aucune zone de mémoire à aucun des niveaux (arène, pool, bloc) jusqu'à ce que cela soit nécessaire".

Cela signifie qu'un bloc peut avoir trois états. Ils peuvent être définis comme suit:

  • Inchangé : zones de mémoire non allouées;
  • Libre : zones de mémoire qui ont été allouées mais libérées plus tard par CPython car elles ne contenaient pas d'informations pertinentes;
  • Distribué : zones de mémoire qui contiennent actuellement des informations actuelles.

Le pointeur freeblock est une liste à liaison unique de blocs de mémoire libres. En d'autres termes, il s'agit d'une liste d'endroits gratuits où vous pouvez écrire des informations. Si plus de mémoire est nécessaire que dans les blocs libres, alors l'allocateur utilise les blocs intacts dans le pool.

Dès que le gestionnaire de mémoire libère des blocs, ces blocs sont ajoutés en haut de la liste des blocs libres. La liste réelle peut ne pas contenir une séquence continue de blocs de mémoire, comme dans la première figure «réussie».



Arènes

Les arènes contiennent des piscines. Les arènes, contrairement aux pools, n'ont pas de divisions étatiques explicites.

Ils sont eux-mêmes organisés en une liste doublement liée appelée la liste des arènes utilisables (arènes_utilisables). Cette liste est triée par nombre de pools gratuits. Moins il y a de piscines gratuites, plus l'arène est proche du haut de la liste.



Cela signifie que l'arène la plus complète sera sélectionnée pour enregistrer encore plus de données. Mais pourquoi exactement? Pourquoi ne pas écrire des données là où se trouve l'espace le plus libre?

Cela nous amène à l'idée de libérer complètement la mémoire. Le fait est que dans certains cas, lorsque la mémoire est libérée, elle reste inaccessible au système d'exploitation. Le processus Python le maintient distribué et l'utilise plus tard pour de nouvelles données. La désallocation complète de la mémoire renvoie la mémoire au système d'exploitation

Les arènes ne sont pas les seules zones qui peuvent être complètement évacuées. Ainsi, nous comprenons que les arènes qui sont sur la liste «plus proche du vide» devraient être libérées. Dans ce cas, la zone mémoire peut vraiment être complètement libérée, et en conséquence, la capacité mémoire totale de votre programme Python est réduite.

Conclusion

La gestion de la mémoire est l'une des parties les plus importantes du travail avec un ordinateur. D'une manière ou d'une autre, Python effectue pratiquement toutes ses opérations en mode furtif.

De cet article, vous avez appris:

  • Qu'est-ce que la gestion de la mémoire et pourquoi est-elle importante?
  • Qu'est-ce que CPython, une implémentation de base de Python;
  • Fonctionnement des structures de données et des algorithmes dans la gestion de la mémoire de CPython et stockage de vos données.

Python résume les nombreuses nuances du travail avec un ordinateur. Cela permet de travailler à un niveau supérieur et de se débarrasser des maux de tête sur le sujet où et comment les octets de votre programme sont stockés.

Nous avons donc appris la gestion de la mémoire en Python. Traditionnellement, nous attendons vos commentaires, et nous vous invitons également à une journée portes ouvertes au cours Python Developer, qui aura lieu le 13 mars

Source: https://habr.com/ru/post/fr443312/


All Articles