Python consomme beaucoup de mémoire ou comment réduire la taille des objets?

Un problème de mémoire peut survenir lorsqu'un grand nombre d'objets sont actifs dans la RAM pendant l'exécution d'un programme, en particulier s'il existe des restrictions sur la quantité totale de mémoire disponible.


Vous trouverez ci-dessous un aperçu de certaines méthodes de réduction de la taille des objets, ce qui peut réduire considérablement la quantité de RAM nécessaire pour les programmes en Python pur.


Remarque: il s'agit de la version anglaise de mon message d' origine (en russe).


Pour simplifier, nous considérerons les structures en Python pour représenter des points avec les coordonnées x , y , z avec accès aux valeurs de coordonnées par leur nom.


Dict


Dans les petits programmes, en particulier dans les scripts, il est assez simple et pratique d'utiliser le dict intégré pour représenter les informations structurelles:


 >>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y 

Avec l'avènement d'une implémentation plus compacte en Python 3.6 avec un ensemble ordonné de clés, dict est devenu encore plus attrayant. Cependant, regardons la taille de son empreinte en RAM:


 >>> print(sys.getsizeof(ob)) 240 

Cela prend beaucoup de mémoire, surtout si vous devez soudainement créer un grand nombre d'instances:


Nombre d'instancesTaille des objets
1 000 000240 Mo
10 000 0002,40 Go
100 000 00024 Go

Instance de classe


Pour ceux qui aiment tout habiller en classes, il est préférable de définir les structures comme une classe avec accès par nom d'attribut:


 class Point: # def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> x = ob.x >>> ob.y = y 

La structure de l'instance de classe est intéressante:


ChampTaille (octets)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
TOTAL:56

Ici, __weakref__ est une référence à la liste des soi-disant références faibles à cet objet, le champ __dict__ est une référence au dictionnaire d'instance de classe, qui contient les valeurs des attributs d'instance (notez que la plate-forme de références 64 bits occupe 8 octets). À partir de Python 3.3, l'espace partagé est utilisé pour stocker des clés dans le dictionnaire pour toutes les instances de la classe. Cela réduit la taille de la trace d'instance dans la RAM:


 >>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112 

Par conséquent, un grand nombre d'instances de classe ont une plus petite empreinte en mémoire qu'un dictionnaire standard ( dict ):


Nombre d'instancesLa taille
1 000 000168 Mo
10 000 0001,68 Go
100 000 00016,8 Go

Il est facile de voir que la taille de l'instance en RAM est toujours importante en raison de la taille du dictionnaire de l'instance.


Instance de classe avec __slots__


Une réduction significative de la taille d'une instance de classe en RAM est obtenue en éliminant __dict__ et __weakref__ . Ceci est possible à l'aide d'une "astuce" avec __slots__ :


 class Point: __slots__ = 'x', 'y', 'z' def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 64 

La taille de l'objet dans la RAM est devenue beaucoup plus petite:


ChampTaille (octets)
PyGC_Head24
PyObject_HEAD16
x8
y8
z8
TOTAL:64

L'utilisation de __slots__ dans la définition de classe réduit considérablement l'encombrement d'un grand nombre d'instances en mémoire:


Nombre d'instancesLa taille
1 000 00064 Mo
10 000 000640 Mo
100 000 0006,4 Go

Actuellement, il s'agit de la principale méthode de réduction substantielle de l'empreinte mémoire d'une instance d'une classe en RAM.


Cette réduction est obtenue par le fait que dans la mémoire après le titre de l'objet, les références d'objet sont stockées - les valeurs d'attribut, et leur accès est effectué à l'aide de descripteurs spéciaux qui se trouvent dans le dictionnaire de classe:


 >>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>}) 

Pour automatiser le processus de création d'une classe avec __slots__ , il existe une bibliothèque [namedlist] ( https://pypi.org/project/namedlist ). La fonction namedlist.namedlist crée une classe avec __slots__ :


 >>> Point = namedlist('Point', ('x', 'y', 'z')) 

Un autre package [attrs] ( https://pypi.org/project/attrs ) vous permet d'automatiser le processus de création de classes avec et sans __slots__ .


Tuple


Python possède également un tuple type intégré pour représenter les structures de données immuables. Un tuple est une structure ou un enregistrement fixe, mais sans nom de champ. Pour l'accès aux champs, l'index des champs est utilisé. Les champs de tuple sont une fois pour toutes associés aux objets valeur au moment de la création de l'instance de tuple:


 >>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y # ERROR 

Les instances de tuple sont assez compactes:


 >>> print(sys.getsizeof(ob)) 72 

Ils occupent 8 octets en mémoire de plus que les instances de classes avec __slots__ , car la trace de tuple en mémoire contient également un certain nombre de champs:


ChampTaille (octets)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
TOTAL:72

Namedtuple


Étant donné que le tuple est très largement utilisé, un jour, il a été demandé que vous puissiez toujours accéder aux champs et par nom également. La réponse à cette demande a été le module collections.namedtuple .


La fonction namedtuple est conçue pour automatiser le processus de génération de ces classes:


 >>> Point = namedtuple('Point', ('x', 'y', 'z')) 

Il crée une sous-classe de tuple, dans laquelle des descripteurs sont définis pour accéder aux champs par leur nom. Pour notre exemple, cela ressemblerait à ceci:


  class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_z(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z)) 

Toutes les instances de ces classes ont une empreinte mémoire identique à celle d'un tuple. Un grand nombre d'instances laisse une empreinte mémoire légèrement plus grande:


Nombre d'instancesLa taille
1 000 00072 Mo
10 000 000720 Mo
100 000 0007,2 Go

Recordclass: mutable nommé tuple sans GC cyclique


Étant donné que le tuple et, par conséquent, les classes namedtuple génèrent des objets immuables dans le sens où l'attribut ob.x ne peut plus être associé à un autre objet de valeur, une demande pour une variante mutable namedtuple est apparue. Puisqu'il n'y a pas de type intégré en Python qui soit identique au tuple qui prend en charge les affectations, de nombreuses options ont été créées. Nous nous concentrerons sur [recordclass] ( https://pypi.org/project/recordclass ), qui a reçu une note de [stackoverflow] ( https://stackoverflow.com/questions/29290359/existence-of-mutable-named- tuple-in -python / 29419745). De plus, il peut être utilisé pour réduire la taille des objets en RAM par rapport à la taille des objets de type tuple .


Le package recordclass introduit le type recordclass.mutabletuple , qui est presque identique au tuple, mais prend également en charge les affectations. Sur sa base, des sous-classes sont créées qui sont presque complètement identiques aux couples nommés, mais prennent également en charge l'affectation de nouvelles valeurs aux champs (sans créer de nouvelles instances). La fonction recordclass , comme la fonction namedtuple , vous permet d'automatiser la création de ces classes:


  >>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3) 

Les instances de classe ont la même structure que tuple , mais uniquement sans PyGC_Head :


ChampTaille (octets)
PyObject_HEAD16
ob_size8
x8
y8
y8
TOTAL:48

Par défaut, la fonction recordclass crée une classe qui ne participe pas au mécanisme de récupération de place cyclique. En règle générale, namedtuple et recordclass sont utilisés pour générer des classes représentant des enregistrements ou des structures de données simples (non récursives). Les utiliser correctement en Python ne génère pas de références circulaires. Pour cette raison, dans le sillage des instances de classes générées par recordclass , par default, the fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the PyGC_Head fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the structure, corresponding to the created class, in the PyTypeObject structure, corresponding to the created class, in the field, by default, the flag drapeaux field, by default, the flag Py_TPFLAGS_HAVE_GC` n'est pas défini).


La taille de l'empreinte mémoire d'un grand nombre d'instances est inférieure à celle des instances de la classe avec __slots__ :


Nombre d'instancesLa taille
1 000 00048 Mo
10 000 000480 Mo
100 000 0004,8 Go

Dataobject


Une autre solution proposée dans la bibliothèque recordclass est basée sur l'idée: utiliser la même structure de stockage en mémoire que dans les instances de classe avec __slots__ , mais ne pas participer au mécanisme cyclique de récupération de place. Ces classes sont générées à l'aide de la fonction recordclass.make_dataclass :


  >>> Point = make_dataclass('Point', ('x', 'y', 'z')) 

La classe ainsi créée, par défaut, crée des instances mutables.


Une autre façon - utilisez la déclaration de classe avec l'héritage de recordclass.dataobject :


 class Point(dataobject): x:int y:int z:int 

Les classes créées de cette manière créeront des instances qui ne participent pas au mécanisme de collecte de déchets cyclique. La structure de l'instance en mémoire est la même que dans le cas avec __slots__ , mais sans PyGC_Head :


ChampTaille (octets)
PyObject_HEAD16
x8
y8
y8
TOTAL:40

 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40 

Pour accéder aux champs, des descripteurs spéciaux sont également utilisés pour accéder au champ par son décalage par rapport au début de l'objet, qui se trouvent dans le dictionnaire de classe:


 mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>, ....................................... 'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>, 'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>, 'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>}) 

La taille de l'empreinte mémoire d'un grand nombre d'instances est le minimum possible pour CPython:


Nombre d'instancesLa taille
1 000 00040 Mo
10 000 000400 Mo
100 000 0004,0 Go

Cython


Il existe une approche basée sur l'utilisation de [Cython] ( https://cython.org ). Son avantage est que les champs peuvent prendre les valeurs des types atomiques du langage C. Des descripteurs pour accéder aux champs à partir de Python pur sont créés automatiquement. Par exemple:


 cdef class Python: cdef public int x, y, z def __init__(self, x, y, z): self.x = x self.y = y self.z = z 

Dans ce cas, les instances ont une taille de mémoire encore plus petite:


 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 32 

La trace d'instance en mémoire a la structure suivante:


ChampTaille (octets)
PyObject_HEAD16
x4
y4
y4
est vide4
TOTAL:32

La taille de l'empreinte d'un grand nombre de copies est moindre:


NuméroLa taille
1 000 00032 Mo
10 000 000320 Mo
100 000 0003,2 Go

Cependant, il faut se rappeler que lors de l'accès à partir du code Python, une conversion de int en objet Python et vice versa sera effectuée à chaque fois.


Numpy


L'utilisation de tableaux multidimensionnels ou de tableaux d'enregistrements pour une grande quantité de données donne un gain de mémoire. Cependant, pour un traitement efficace en Python pur, vous devez utiliser des méthodes de traitement qui se concentrent sur l'utilisation des fonctions du package numpy .


 >>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)]) 

Un tableau de N éléments, initialisé avec des zéros, est créé à l'aide de la fonction:


  >>> points = numpy.zeros(N, dtype=Point) 

La taille du tableau en mémoire est le minimum possible:


Nombre d'objetsLa taille
1 000 00012 Mo
10 000 000120 Mo
100 000 0001,20 Go

L'accès normal aux éléments et aux lignes du tableau nécessitera la conversion d'un objet Python en une valeur C int et vice versa. L'extraction d'une seule ligne entraîne la création d'un tableau contenant un seul élément. Sa trace ne sera plus aussi compacte:


  >>> sys.getsizeof(points[0]) 68 

Par conséquent, comme indiqué ci-dessus, dans le code Python, il est nécessaire de traiter les tableaux à l'aide des fonctions du package numpy .


Conclusion


Sur un exemple clair et simple, il a été possible de vérifier que la communauté de développeurs et d'utilisateurs du langage de programmation Python (CPython) a de réelles possibilités pour une réduction significative de la quantité de mémoire utilisée par les objets.

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


All Articles