Comprendre la quantité de mémoire utilisée par vos objets Python

Python est un langage de programmation fantastique. Il est également connu pour être assez lent, en raison principalement de son énorme flexibilité et de ses fonctionnalités dynamiques. Pour de nombreuses applications et domaines, cela ne pose pas de problème en raison de leurs exigences et de diverses techniques d'optimisation. On sait moins que les graphes d’objets Python (dictionnaires imbriqués de listes, de nuplets et de types primitifs) utilisent une quantité de mémoire importante. Cela peut constituer un facteur limitant beaucoup plus important en raison de ses effets sur la mise en cache, la mémoire virtuelle, la multi-location avec d'autres programmes et en général, l'épuisement plus rapide de la mémoire disponible, ressource rare et coûteuse..

Il s'avère qu'il n'est pas facile de déterminer la quantité de mémoire réellement utilisée. Dans cet article, je vais vous expliquer les subtilités de la gestion de la mémoire d'un objet Python et vous montrer comment mesurer avec précision la mémoire consommée..

Dans cet article, je me concentre uniquement sur CPython, la principale implémentation du langage de programmation Python. Les expériences et les conclusions ici ne s'appliquent pas aux autres implémentations Python telles que IronPython, Jython et PyPy.

Aussi, j'ai couru les nombres sur Python 2.7 64 bits. Dans Python 3, les nombres sont parfois un peu différents (surtout pour les chaînes qui sont toujours Unicode), mais les concepts sont les mêmes..

Exploration pratique de l'utilisation de la mémoire Python

D'abord, explorons un peu et obtenons une idée concrète de l'utilisation réelle de la mémoire des objets Python.

La fonction intégrée sys.getsizeof ()

Le module sys de la bibliothèque standard fournit la fonction getsizeof (). Cette fonction accepte un objet (et la valeur par défaut optionnelle), appelle le taille de() et retourne le résultat pour que vos objets puissent aussi être inspectés.

Mesurer la mémoire d'objets Python

Commençons par quelques types numériques:

"système d'importation python

sys.getsizeof (5) 24 "

Intéressant. Un entier prend 24 octets.

python sys.getsizeof (5.3) 24

Hmm… un float prend également 24 octets.

python à partir de l'importation décimale Decimal sys.getsizeof (Decimal (5.3)) 80

Sensationnel. 80 octets! Cela vous fait vraiment réfléchir à la question de savoir si vous voulez représenter un grand nombre de nombres réels sous forme de flottants ou de nombres décimaux..

Passons aux chaînes et aux collections:

"python sys.getsizeof (") 37 sys.getsizeof ('1') 38 sys.getsizeof ('1234') 41

sys.getsizeof (u ") 50 sys.getsizeof (u'1 ') 52 sys.getsizeofof (u'1234') 58"

D'ACCORD. Une chaîne vide prend 37 octets et chaque caractère supplémentaire ajoute un autre octet. Cela en dit long sur le compromis entre garder plusieurs chaînes courtes où vous payez la surcharge de 37 octets pour chacune par rapport à une seule chaîne longue où vous ne payez la surcharge qu'une seule fois..

Les chaînes Unicode se comportent de la même manière, sauf que la surcharge est de 50 octets et que chaque caractère supplémentaire ajoute 2 octets. C'est quelque chose à considérer si vous utilisez des bibliothèques qui renvoient des chaînes Unicode, mais votre texte peut être représenté sous forme de chaînes simples..

En passant, dans Python 3, les chaînes sont toujours au format Unicode et la surcharge est de 49 octets (elles ont enregistré un octet quelque part). L'objet octets a une surcharge de seulement 33 octets. Si vous avez un programme qui traite beaucoup de chaînes courtes en mémoire et que vous vous souciez de la performance, envisagez Python 3..

python sys.getsizeof ([]) 72 sys.getsizeof ([1]) 88 sys.getsizeof ([1, 2, 3, 4]) 104 sys.getsizeof (['une longue chaîne longue'])

Que se passe-t-il? Une liste vide prend 72 octets, mais chaque entier supplémentaire n'ajoute que 8 octets, la taille d'un entier étant de 24 octets. Une liste contenant une longue chaîne ne prend que 80 octets..

La réponse est simple La liste ne contient pas les objets int eux-mêmes. Il ne contient qu'un pointeur de 8 octets (sur les versions 64 bits de CPython) vers l'objet int réel. Cela signifie que la fonction getsizeof () ne renvoie pas la mémoire réelle de la liste et tous les objets qu'elle contient, mais uniquement la mémoire de la liste et les pointeurs sur ses objets. Dans la section suivante, je présenterai la fonction deep_getsizeof () qui résout ce problème..

python sys.getsizeof (()) 56 sys.getsizeof ((1,)) 64 sys.getsizeof ((1, 2, 3, 4)) 88 sys.getsizeof (('une longue chaîne longue',)) 64

L'histoire est similaire pour les tuples. La surcharge d'un tuple vide est de 56 octets contre 72 sur une liste. Encore une fois, cette différence de 16 octets par séquence est une tâche facile si vous avez une structure de données avec beaucoup de petites séquences immuables..

"python sys.getsizeof (set ()) 232 sys.getsizeof (set ([1)) 232 sys.getsizeof of (set ([1, 2, 3, 4])) 232

sys.getsizeof () 280 sys.getsizeof (dict (a = 1)) 280 sys.getsizeofof (dict (a = 1, b = 2, c = 3)) 280 "

Les ensembles et les dictionnaires ne grandissent apparemment pas du tout lorsque vous ajoutez des éléments, mais notez l'énorme surcharge.

L'essentiel est que les objets Python ont une surcharge énorme fixe. Si votre structure de données est composée d'un grand nombre d'objets de collection tels que des chaînes, des listes et des dictionnaires contenant chacun un petit nombre d'éléments, vous payez un lourd tribut..

La fonction deep_getsizeof ()

Maintenant que je t'ai effrayé à moitié et que j'ai également démontré que sys.getsizeof () ne peut que te dire combien de mémoire prend un objet primitif, examinons une solution plus adéquate. La fonction deep_getsizeof () explore de manière récursive et calcule l'utilisation réelle de la mémoire d'un graphe d'objet Python..

"python des collections import Mapping, Container from sys import getsizeof

def deep_getsizeof (o, ids): "" ”Recherche l'empreinte mémoire d'un objet Python

Il s'agit d'une fonction récursive qui explore un graphe d'objet Python comme un dictionnaire contenant des dictionnaires imbriqués avec des listes de listes, de tuples et d'ensembles. La fonction sys.getsizeof fait une taille peu profonde de seulement. Chaque objet à l'intérieur d'un conteneur est compté comme un pointeur uniquement, quelle que soit sa taille. : param o: l'objet: param ids:: return: "" "d = deep_getsizeof if id (o) dans ids: return 0 r = getsizeof (o) ids.add (id (o)) si isinstance (o, str ) ou isinstance (0, unicode): renvoie r si est une instance (o, mappage): renvoie r + somme (d (k, ids) + d (v, identifiants) pour k, v dans o.iteritems ()) si isinstance (o, Container): retourne r + somme (d (x, ids) pour x dans o) retourne "

Cette fonction présente plusieurs aspects intéressants. Il prend en compte les objets référencés plusieurs fois et ne les compte qu'une seule fois en gardant une trace des identifiants d'objet. L'autre caractéristique intéressante de cette implémentation est qu'elle tire pleinement parti des classes de base abstraites du module de collections. Cela permet à la fonction de gérer de manière très précise toute collection implémentant les classes de base Mapping ou Container au lieu de traiter directement avec une myriade de types de collection tels que: chaîne, Unicode, octets, liste, tuple, dict, frozendict, OrderedDict, set, frozenset, etc..

Voyons le en action:

python x = '1234567' deep_getsizeof (x, set ()) 44

Une chaîne de longueur 7 prend 44 octets (37 surcharge + 7 octets pour chaque caractère).

python deep_getsizeof ([], set ()) 72

Une liste vide prend 72 octets (juste au-dessus).

python deep_getsizeof ([x], set ()) 124

Une liste contenant la chaîne x prend 124 octets (72 + 8 + 44).

python deep_getsizeof ([x, x, x, x, x], set ()) 156

Une liste contenant la chaîne x 5 fois prend 156 octets (72 + 5 * 8 + 44).

Le dernier exemple montre que deep_getsizeof () compte les références au même objet (la chaîne x) une seule fois, mais que le pointeur de chaque référence est compté..

Friandises ou astuces

Il s'avère que CPython a plusieurs astuces dans sa manche, donc les nombres que vous obtenez de deep_getsizeof () ne représentent pas entièrement l'utilisation de la mémoire d'un programme Python..

Comptage de références

Python gère la mémoire en utilisant la sémantique du comptage de références. Une fois qu'un objet n'est plus référencé, sa mémoire est désallouée. Mais tant qu'il y a une référence, l'objet ne sera pas désalloué. Des choses comme les références cycliques peuvent vous mordre assez fort.

Petits objets

CPython gère les petits objets (moins de 256 octets) dans des pools spéciaux sur des limites de 8 octets. Il existe des pools pour 1 à 8 octets, 9 à 16 octets et jusqu'à 249-256 octets. Lorsqu'un objet de taille 10 est alloué, il est alloué à partir du pool de 16 octets pour les objets d'une taille comprise entre 9 et 16 octets. Ainsi, même s'il ne contient que 10 octets de données, il vous en coûtera 16 octets de mémoire. Si vous allouez 1 000 000 objets de taille 10, vous utilisez en réalité 16 000 000 octets et non 10 000 000 octets, comme vous pouvez le supposer. Ces 60% de frais généraux ne sont évidemment pas anodins.

Entiers

CPython conserve une liste globale de tous les entiers compris dans l'intervalle [-5, 256]. Cette stratégie d’optimisation est logique car de petits entiers apparaissent partout et, comme chaque entier prend 24 octets, cela économise beaucoup de mémoire pour un programme typique..

Cela signifie également que CPython pré-alloue 266 * 24 = 6384 octets pour tous ces entiers, même si vous n'en utilisez pas la plupart. Vous pouvez le vérifier en utilisant la fonction id () qui donne le pointeur sur l'objet réel. Si vous appelez id (x) multiple pour tout x de la plage [-5, 256], vous obtiendrez le même résultat à chaque fois (pour le même entier). Mais si vous l’essayez pour des entiers situés en dehors de cette plage, ils seront différents (un nouvel objet est créé à la volée à chaque fois)..

Voici quelques exemples dans la gamme:

"identifiant python (-3) 140251817361752

id (-3) 140251817361752

id (-3) 140251817361752

id (201) 140251817366736

id (201) 140251817366736

id (201) 140251817366736 "

Voici quelques exemples hors gamme:

"identifiant python (301) 140251846945800

id (301) 140251846945776

id (-6) 140251846946960

id (-6) 140251846946936 "

Mémoire Python vs mémoire système

CPython est un peu possessif. Dans de nombreux cas, lorsque les objets mémoire de votre programme ne sont plus référencés, ils le sont. ne pas retournés au système (par exemple, les petits objets). C'est bon pour votre programme si vous allouez et libérez de nombreux objets (appartenant au même pool de 8 octets), car Python n'a pas à déranger le système, ce qui est relativement coûteux. Mais ce n’est pas très bien si votre programme utilise normalement X octets et dans certaines conditions temporaires, il en utilise 100 fois plus (par exemple, l’analyse et le traitement d’un gros fichier de configuration uniquement au démarrage).

Maintenant, cette mémoire 100X peut être inutilement piégée dans votre programme, ne jamais être utilisée à nouveau et empêcher le système de l'attribuer à d'autres programmes. L'ironie est que si vous utilisez le module de traitement pour exécuter plusieurs instances de votre programme, vous limiterez sévèrement le nombre d'instances que vous pouvez exécuter sur une machine donnée..

Memory Profiler

Pour évaluer et mesurer l'utilisation réelle de la mémoire de votre programme, vous pouvez utiliser le module memory_profiler. J'ai un peu joué avec et je ne suis pas sûr de faire confiance aux résultats. Son utilisation est très simple. Vous décorez une fonction (la fonction principale (0)) avec @profiler decorator et, lorsque le programme se ferme, le profileur de mémoire imprime en sortie standard un rapport pratique indiquant le total et les changements en mémoire pour chaque ligne. Voici un exemple. programme que j'ai exécuté sous le profileur:

"python depuis le profil d’importation memory_profiler

@profile def main (): a = [] b = [] c = [] pour i dans l'intervalle (100000): a.append (5) pour i dans l'intervalle (100000): b.append (300) pour i dans plage (100000): c.append ('123456789012345678901234567890') del a del b del c

imprimer 'Fait!' si __name__ == '__main__': main () "

Voici la sortie:

Line # Mem utilisation Increment Line Content ========================================= ===== 3 22,9 Mio 0,0 Mio @ profil 4 def principal (): 5 22,9 Mio 0,0 Mio a = [] 6 22,9 Mio 0,0 Mio b = [] 7 22,9 Mio 0,0 Mio c = [] 8 27,1 Mio 4,2 Mio pour i dans la plage (100000): 9 27,1 Mio 0,0 MiB a.append (5) 10 27,5 Mio 0,4 Mio pour i dans la plage (100000): 11 27,5 Mio 0,0 MiB b.ajout (300) 12 28,3 MiB à 0,8 MiB pour i dans les limites (100000): 13 28,3 Mio 0,0 Mio c.append ('123456789012345678901234567890') 14 27,7 Mio -0,6 Mio a 15 27,9 Mio 0,2 Mio a B 16 27,3 Mio a -0,6 M 17 Cm 17 18 27,3 Mio 0,0 Mio print ' Terminé!' 

Comme vous pouvez le constater, 22,9 Mo de mémoire vive sont utilisés. La raison pour laquelle la mémoire n'augmente pas lors de l'ajout d'entiers à la fois à l'intérieur et à l'extérieur de la plage [-5, 256] et lors de l'ajout de la chaîne est qu'un seul objet est utilisé dans tous les cas. La raison pour laquelle la première boucle de plage (100 000) sur la ligne 8 ajoute 4,2 Mo alors que la seconde sur la ligne 10 n’ajoute que 0,4 Mo et la troisième boucle sur la ligne 12 ajoute 0,8 Mo. Enfin, lors de la suppression des listes a, b et c, -0,6 Mo est libéré pour a et c, mais pour b 0,2 Mo est ajouté. Je ne peux pas avoir beaucoup de sens de ces résultats.

Conclusion

CPython utilise beaucoup de mémoire pour ses objets. Il utilise diverses astuces et optimisations pour la gestion de la mémoire. En gardant une trace de l'utilisation de la mémoire de votre objet et du modèle de gestion de la mémoire, vous pouvez réduire considérablement l'empreinte mémoire de votre programme..

Apprendre le python

Apprenez Python avec notre guide complet de tutoriel sur Python, que vous soyez débutant ou que vous soyez un codeur chevronné cherchant à acquérir de nouvelles compétences..