Gestion de la mémoire Python: un peu sur la fragmentation de la mémoire

Quelques réflexions sur cet article .

Récemment, je me suis intéressé au fonctionnement de Python Memory Management dans CPython pour Python3 pour Ubuntu 64 bits .

Un peu de théorie


La bibliothèque système glibc possède un allocateur malloc. Chaque processus a une zone mémoire appelée tas. En allouant de la mémoire dynamiquement en appelant la fonction malloc, nous obtenons un morceau du tas de ce processus. Si la taille de la mémoire demandée est petite (pas plus de 128 Ko), la mémoire peut être extraite des listes de morceaux libres. Si cela n'est pas possible, la mémoire sera allouée à l'aide de l'appel système mmap (sbrk, brk) . L'appel système mmap mappe la mémoire virtuelle à la mémoire physique. La mémoire est affichée en pages 4KB. Les gros morceaux (plus de 128 Ko) sont toujours alloués via l'appel système mmap. Lors de la libération de mémoire, si un petit morceau libre borde une zone de mémoire non gelée, une partie de la mémoire peut revenir au système d'exploitation. Les gros morceaux reviennent immédiatement au système d'exploitation.

Informations tirées d'une conférence sur les allocateurs en C.

CPython a son propre allocateur (PyMalloc) pour le "tas privé" et des allocateurs pour chaque type d'objet qui fonctionne "au-dessus" du premier. PyMalloc demande des blocs de mémoire de 256 Ko via malloc dans la bibliothèque du système d'exploitation appelée Arenas. Ils, à leur tour, sont divisés en pools de 4 Ko. Chaque pool est divisé en morceaux d'une taille fixe, et chacun peut être divisé en morceaux d'une des 64 tailles.

Les allocateurs pour chaque type utilisent les morceaux déjà alloués, le cas échéant. S'il n'y en a pas, PyMalloc émettra une nouvelle piscine depuis la première arène, dans laquelle il y a une place pour une nouvelle piscine (les arènes sont «triées» par ordre décroissant d'occupation). Si cela ne fonctionne pas, PyMalloc demande au système d'exploitation une nouvelle arène. Sauf lorsque la taille de mémoire demandée est supérieure à 512B, la mémoire est directement allouée via malloc à partir de la bibliothèque système.

Lorsqu'un objet est supprimé, la mémoire n'est pas renvoyée au système d'exploitation, mais les blocs retournent simplement aux pools correspondants et les pools aux arènes. L'arène revient au système d'exploitation lorsque tous les morceaux en sont libérés. D'après ce qu'il s'avère, si un nombre relativement petit de morceaux sera utilisé dans l'arène, alors tout de même toute la mémoire dans l'arène sera utilisée par PVM. Mais comme des morceaux de plus de 128 Ko sont alloués via mmap, l'arène gratuite reviendra immédiatement au système d'exploitation.

Je voudrais me concentrer sur deux points:

  1. Il s'avère que PyMalloc alloue 256 Ko de mémoire physique lors de la création d'une nouvelle arène.
  2. Seules les arènes gratuites sont retournées au système d'exploitation.

Exemple


Prenons l'exemple suivant:

iterations = 2000000 l = [] for i in range(iterations): l.append(None) for i in range(iterations): l[i] = {} s = [] # [1] # s = l[::2] # [2] # s = l[2000000 // 2::] # [3] # s = l[::100] # [4] for i in range(iterations): l[i] = None for i in range(iterations): l[i] = {} 

Dans l'exemple, une liste l de 2 millions d'éléments est créée, qui pointent tous vers un objet None. Dans le cycle suivant, un objet est créé pour chaque élément - un dictionnaire vide. Ensuite, une deuxième liste s est créée, dont les éléments pointent vers certains objets référencés par certains éléments de la première liste. Après l'analyse suivante, les éléments de la liste l recommencent à pointer vers l'objet Aucun. Et dans le dernier cycle, des dictionnaires sont à nouveau créés pour chaque élément de la première liste.

Options de la liste S:

  1.  s = [] 
  2.  s = l[::2] 
  3.  s = l[200000 // 2::] 
  4.  s = l[::100] 

Nous nous intéressons à la consommation de mémoire dans chaque cas.

Nous exécuterons ce script avec la journalisation PyMalloc activée:

 export PYTHONMALLOCSTATS="True" && python3 source.py 2>result.txt 

Explication des résultats


Dans les images, vous pouvez voir la consommation de mémoire dans chaque cas. Sur l'axe des abscisses, il n'y a pas de corrélation des valeurs avec le moment où cette consommation a eu lieu, seule chaque valeur dans les journaux est associée à son numéro de série.

image

«Sans éléments»


Dans le premier cas, la liste s est vide. Après avoir créé les objets dans le deuxième cycle, environ 500 Mo de mémoire sont consommés. Et tous ces objets sont supprimés au cours du troisième cycle, et la mémoire utilisée est retournée au système d'exploitation. Dans le dernier cycle, la mémoire des objets est à nouveau allouée, ce qui entraîne la consommation des mêmes 500 Mo.

"Chaque seconde"


Dans le cas où nous créons une liste avec chaque deuxième élément de la liste l, nous pouvons remarquer que la mémoire n'est pas retournée au système d'exploitation. Autrement dit, dans ce cas, nous observons une situation où les dictionnaires d'environ 250 Mo sont supprimés, mais dans chaque pool, il y a des morceaux qui ne sont pas supprimés, à cause desquels les arènes correspondantes ne sont pas publiées. Mais, lorsque nous créons les dictionnaires pour la deuxième fois, les morceaux gratuits de ces pools sont réutilisés, c'est pourquoi seulement environ 250 Mo de nouvelle mémoire sont alloués.

"La seconde moitié"


Dans le cas où nous créons une liste à partir de la seconde moitié des éléments de la liste l, la première moitié est dans des arènes distinctes, grâce auxquelles environ 250 Mo de mémoire sont retournés au système d'exploitation. Après cela, environ 500 Mo sont réaffectés à de nouveaux dictionnaires, ce qui explique la consommation totale dans la région de 750 Mo.

Dans ce cas, contrairement au second, la mémoire est partiellement restituée au système d'exploitation. Ce qui, d'une part, permet à d'autres processus d'utiliser cette mémoire, d'autre part, nécessite des appels système pour la libérer et la réallouer.

"Chaque centième"


Le dernier cas semble être le plus intéressant. Là, nous créons une deuxième liste à partir de chaque centième élément de la première liste, ce qui nécessite environ 5 Mo. Mais du fait qu'un certain nombre de Morceaux occupés restent dans chaque Arène, cette mémoire n'est pas libérée et la consommation reste au niveau de 500 Mo. Lorsque nous créons des dictionnaires pour la deuxième fois, presque aucune nouvelle mémoire n'est allouée et les morceaux alloués pour la première fois sont réutilisés.

Dans cette situation, en raison de la fragmentation de la mémoire, nous utilisons 100 fois plus que ce dont nous avons besoin. Mais, lorsque cette mémoire est requise à plusieurs reprises, nous n'avons pas à effectuer d'appels système pour l'allouer.

Résumé


Il convient de noter que la fragmentation de la mémoire est possible lors de l'utilisation de nombreux allocateurs. Néanmoins, vous devez utiliser soigneusement certaines structures de données, par exemple celles qui ont une structure arborescente, telles que les arbres de recherche. Parce que des opérations arbitraires d'ajout et de suppression peuvent conduire à la situation décrite ci-dessus, à cause de laquelle l'opportunité d'utiliser ces structures en termes de consommation de mémoire sera mise en doute.

Code de rendu des images
 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) 

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


All Articles