web-dev-qa-db-fra.com

Pourquoi les n-uplets prennent-ils moins de place en mémoire que les listes?

Un Tuple prend moins d'espace mémoire en Python:

>>> a = (1,2,3)
>>> a.__sizeof__()
48

alors que lists prend plus d’espace mémoire:

>>> b = [1,2,3]
>>> b.__sizeof__()
64

Que se passe-t-il en interne sur la gestion de la mémoire Python?

99
JON

Je suppose que vous utilisez CPython et 64 bits (les mêmes résultats sont obtenus avec mon CPython 2.7 64 bits). Il pourrait y avoir des différences entre d’autres implémentations Python ou avec un Python 32 bits.

Quelle que soit l'implémentation, les list sont de taille variable, tandis que Tuples sont de taille fixe.

Donc, Tuples peut stocker les éléments directement à l'intérieur de la structure, les listes ont par contre besoin d'une couche d'indirection (elle stocke un pointeur sur les éléments). Cette couche d'indirection est un pointeur, sur les systèmes 64 bits, 64 bits, donc 8 octets.

Mais lists fait autre chose: ils sur-attribuent. Sinon _list.append_ serait une opération O(n) toujours - pour l'amortir O(1) (beaucoup plus rapidement !!! ) il sur-alloue. Mais maintenant, il doit garder trace de la taille allouée et du rempli taille (Tuples n'a besoin de stocker qu'une taille, car les tailles allouées et remplies sont toujours identiques). Cela signifie que chaque liste doit stocker une autre "taille" qui, sur les systèmes 64 bits, est un entier 64 bits, à nouveau 8 octets.

Donc, lists a besoin d'au moins 16 octets de mémoire de plus que Tuples. Pourquoi ai-je dit "au moins"? En raison de la surallocation. Une surallocation signifie qu'il alloue plus d'espace que nécessaire. Toutefois, le montant de la surallocation dépend de la manière dont vous créez la liste et de l'historique des ajouts/suppressions:

_>>> l = [1,2,3]
>>> l.__sizeof__()
64
>>> l.append(4)  # triggers re-allocation (with over-allocation), because the original list is full
>>> l.__sizeof__()
96

>>> l = []
>>> l.__sizeof__()
40
>>> l.append(1)  # re-allocation with over-allocation
>>> l.__sizeof__()
72
>>> l.append(2)  # no re-alloc
>>> l.append(3)  # no re-alloc
>>> l.__sizeof__()
72
>>> l.append(4)  # still has room, so no over-allocation needed (yet)
>>> l.__sizeof__()
72
_

Images

J'ai décidé de créer des images pour accompagner l'explication ci-dessus. Peut-être que ce sont utiles

Voici comment (schématiquement) il est stocké en mémoire dans votre exemple. J'ai souligné les différences avec les cycles rouges (à main levée):

enter image description here

Ce n'est en fait qu'une approximation, car les objets int sont également des objets Python et CPython réutilise même de petits entiers. Par conséquent, une représentation probablement plus précise (même si elle n'est pas aussi lisible) des objets en mémoire est la suivante:

enter image description here

Liens utiles:

Notez que ___sizeof___ ne renvoie pas vraiment la taille "correcte"! Il ne renvoie que la taille des valeurs stockées. Cependant, lorsque vous utilisez sys.getsizeof , le résultat est différent:

_>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72
_

Il y a 24 octets "extra". Ce sont réels , c'est la surcharge du récupérateur de place qui n'est pas prise en compte dans la méthode ___sizeof___. C’est parce que vous n’êtes généralement pas censé utiliser directement les méthodes magiques - utilisez les fonctions qui savent les manipuler, dans ce cas: sys.getsizeof (ce qui en fait ajoute le GC overhead à la valeur renvoyée par ___sizeof___).

135
MSeifert

Je vais plonger plus profondément dans la base de code CPython afin de voir comment les tailles sont réellement calculées. Dans votre exemple spécifique , aucune sur-allocation n'a été effectuée, je ne vais donc pas aborder ce sujet .

Je vais utiliser les valeurs 64 bits ici, comme vous l'êtes.


La taille de lists est calculée à partir de la fonction suivante, list_sizeof :

_static PyObject *
list_sizeof(PyListObject *self)
{
    Py_ssize_t res;

    res = _PyObject_SIZE(Py_TYPE(self)) + self->allocated * sizeof(void*);
    return PyInt_FromSsize_t(res);
}
_

Ici Py_TYPE(self) est une macro qui saisit le _ob_type_ de self (renvoyant _PyList_Type_) tandis que __PyObject_SIZE_ est une autre macro qui saisit tp_basicsize de ce type. _tp_basicsize_ est calculé comme suit: sizeof(PyListObject)PyListObject est la structure de l'instance.

La structure PyListObject comporte trois champs:

_PyObject_VAR_HEAD     # 24 bytes 
PyObject **ob_item;   #  8 bytes
Py_ssize_t allocated; #  8 bytes
_

ceux-ci ont des commentaires (que j'ai coupés) expliquant ce qu'ils sont, suivez le lien ci-dessus pour les lire. PyObject_VAR_HEAD se développe en trois champs de 8 octets (_ob_refcount_, _ob_type_ et _ob_size_) donc une contribution de _24_.

Donc pour l'instant res c'est:

_sizeof(PyListObject) + self->allocated * sizeof(void*)
_

ou:

_40 + self->allocated * sizeof(void*)
_

Si l'instance de liste a des éléments qui sont alloués. la deuxième partie calcule leur contribution. _self->allocated_, comme son nom l’indique, contient le nombre d’éléments attribués.

Sans aucun élément, la taille des listes est calculée comme suit:

_>>> [].__sizeof__()
40
_

c'est-à-dire la taille de la structure d'instance.


Les objets Tuple ne définissent pas de fonction _Tuple_sizeof_. Au lieu de cela, ils utilisent object_sizeof pour calculer leur taille:

_static PyObject *
object_sizeof(PyObject *self, PyObject *args)
{
    Py_ssize_t res, isize;

    res = 0;
    isize = self->ob_type->tp_itemsize;
    if (isize > 0)
        res = Py_SIZE(self) * isize;
    res += self->ob_type->tp_basicsize;

    return PyInt_FromSsize_t(res);
}
_

Ceci, comme pour lists, saisit le _tp_basicsize_ et, si l'objet a un _tp_itemsize_ différent de zéro (ce qui signifie qu'il a des instances de longueur variable), il multiplie le nombre d'éléments dans le Tuple (qu'il obtient via Py_SIZE ) avec _tp_itemsize_.

_tp_basicsize_ utilise à nouveau sizeof(PyTupleObject) où la structure PyTupleObject contient :

_PyObject_VAR_HEAD       # 24 bytes 
PyObject *ob_item[1];   # 8  bytes
_

Donc, sans aucun élément (c'est-à-dire _Py_SIZE_ renvoie _0_), la taille des n-uplets vides est égale à sizeof(PyTupleObject):

_>>> ().__sizeof__()
24
_

hein? Eh bien, voici une particularité pour laquelle je n'ai pas trouvé d'explication: le _tp_basicsize_ de Tuples est en fait calculé comme suit:

_sizeof(PyTupleObject) - sizeof(PyObject *)
_

pourquoi un _8_ octets supplémentaire est supprimé de _tp_basicsize_ est quelque chose que je n'ai pas pu trouver. (Voir le commentaire de MSeifert pour une explication possible)


Mais, c’est fondamentalement la différence dans votre exemple spécifique . lists conserve également un certain nombre d’éléments alloués, ce qui aide à déterminer le moment opportun pour effectuer une nouvelle surallocation.

Désormais, lorsque des éléments supplémentaires sont ajoutés, les listes effectuent effectivement cette sur-allocation afin d’obtenir O(1) addends. Il en résulte des tailles plus grandes que celles de MSeifert dans sa réponse.

30

La réponse de MSeifert le couvre au sens large; pour rester simple, vous pouvez penser à:

Tuple est immuable. Une fois qu'il est défini, vous ne pouvez pas le changer. Vous savez donc à l'avance combien de mémoire vous devez allouer pour cet objet.

list est modifiable. Vous pouvez ajouter ou supprimer des éléments. Il doit en connaître la taille (pour les impl. Internes). Il redimensionne au besoin.

Il n'y a pas de repas gratuits - ces fonctionnalités ont un coût. D'où la surcharge en mémoire pour les listes.

29
Chen A.

La taille du tuple est préfixée, ce qui signifie qu’à l’initialisation du tuple, l’interprète alloue suffisamment d’espace aux données contenues, ce qui en fait un élément immuable (non modifiable), alors qu’une liste est un objet modifiable, ce qui implique une dynamique. allocation de mémoire, afin d'éviter d'allouer de l'espace chaque fois que vous ajoutez ou modifiez la liste (allouez suffisamment d'espace pour contenir les données modifiées et copiez les données dessus), il alloue de l'espace supplémentaire pour les ajouts, les modifications, etc. résume.

3
rachid el kedmiri