161 votes

Pourquoi deux listes identiques ont-elles une empreinte mémoire différente ?

J'ai créé deux listes l1 y l2 mais chacune avec une méthode de création différente :

import sys

l1 = [None] * 10
l2 = [None for _ in range(10)]

print('Size of l1 =', sys.getsizeof(l1))
print('Size of l2 =', sys.getsizeof(l2))

Mais le résultat m'a surpris :

Size of l1 = 144
Size of l2 = 192

La liste créée avec une compréhension de liste est plus grande en mémoire, mais les deux listes sont identiques en Python par ailleurs.

Pourquoi ça ? Est-ce un truc interne à CPython, ou une autre explication ?

2 votes

Probablement, l'opérateur de répétition invoquera une fonction qui dimensionne exactement le tableau sous-jacent. Notez que 144 == sys.getsizeof([]) + 8*10) où 8 est la taille d'un pointeur.

1 votes

Notez que si vous modifiez 10 a 11 El [None] * 11 la liste a une taille 152 mais la compréhension de la liste a toujours une taille 192 . La question précédemment citée n'est pas un doublon exact, mais elle est pertinente pour comprendre pourquoi cela se produit.

175voto

interjay Points 51000

Lorsque vous écrivez [None] * 10 Python sait qu'il aura besoin d'une liste d'exactement 10 objets, il alloue donc exactement cela.

Lorsque vous utilisez une compréhension de liste, Python ne sait pas de combien il aura besoin. Il fait donc croître progressivement la liste au fur et à mesure que des éléments sont ajoutés. Pour chaque réaffectation, il alloue plus d'espace que ce qui est immédiatement nécessaire, afin de ne pas avoir à réaffecter pour chaque élément. La liste résultante sera probablement un peu plus grande que nécessaire.

Vous pouvez constater ce comportement en comparant des listes créées avec des tailles similaires :

>>> sys.getsizeof([None]*15)
184
>>> sys.getsizeof([None]*16)
192
>>> sys.getsizeof([None for _ in range(15)])
192
>>> sys.getsizeof([None for _ in range(16)])
192
>>> sys.getsizeof([None for _ in range(17)])
264

Vous pouvez voir que la première méthode alloue juste ce qui est nécessaire, tandis que la seconde croît périodiquement. Dans cet exemple, elle alloue suffisamment pour 16 éléments, et a dû réallouer lorsqu'elle a atteint le 17ème.

2 votes

Oui, c'est logique. Il est probablement préférable de créer des listes avec * quand je connais la taille à l'avance.

27 votes

@AndrejKesely N'utiliser que [x] * n avec immuable x dans votre liste. La liste résultante contiendra des références à l'objet identique.

5 votes

@schwobaseggl bien, que mai être ce que vous voulez, mais il est bon de le comprendre.

53voto

juanpa.arrivillaga Points 35811

Comme indiqué dans cette question la liste-compréhension utilise list.append sous le capot, donc il appellera la méthode list-resize, qui sur-alloue.

Pour le démontrer par vous-même, vous pouvez utiliser la fonction dis disséminateur :

>>> code = compile('[x for x in iterable]', '', 'eval')
>>> import dis
>>> dis.dis(code)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x10560b810, file "", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (iterable)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x10560b810, file "", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (x)
              8 LOAD_FAST                1 (x)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE
>>>

Remarquez le LIST_APPEND dans le désassemblage de l <listcomp> objet de code. De la docs :

LIST_APPEND(i)

Appels list.append(TOS[-i], TOS) . Utilisé pour implémenter les compréhensions de listes.

Maintenant, pour l'opération liste-répétition, nous avons un indice sur ce qui se passe si nous considérons :

>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144

Donc, il semble être capable de exactement allouer la taille. En regardant le code source nous voyons que c'est exactement ce qui se passe :

static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
    Py_ssize_t i, j;
    Py_ssize_t size;
    PyListObject *np;
    PyObject **p, **items;
    PyObject *elem;
    if (n < 0)
        n = 0;
    if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
        return PyErr_NoMemory();
    size = Py_SIZE(a) * n;
    if (size == 0)
        return PyList_New(0);
    np = (PyListObject *) PyList_New(size);

A savoir, ici : size = Py_SIZE(a) * n; . Le reste des fonctions remplit simplement le tableau.

0 votes

"Comme indiqué dans cette question, la compréhension de la liste utilise list.append sous le capot" Je pense qu'il est plus exact de dire qu'elle utilise .extend() .

0 votes

@Accumulation pourquoi croyez-vous cela ?

0 votes

Parce qu'il ne s'agit pas d'ajouter des éléments un par un. Lorsque vous ajoutez des éléments à une liste, vous créez réellement une nouvelle liste, avec une nouvelle allocation de mémoire, et vous placez la liste dans cette nouvelle allocation de mémoire. Les comprehensions de listes, en revanche, placent la plupart des nouveaux éléments dans la mémoire qui a déjà été allouée, et lorsqu'elles n'ont plus de mémoire allouée, elles allouent un autre morceau de mémoire, et non pas juste assez pour le nouvel élément.

3voto

StevenJD Points 49

None est un bloc de mémoire, mais il n'a pas une taille prédéfinie. En outre, dans un tableau, il y a un espace supplémentaire entre les éléments du tableau. Vous pouvez le constater vous-même en exécutant :

for ele in l2:
    print(sys.getsizeof(ele))

>>>>16
16
16
16
16
16
16
16
16
16

Ce qui ne totalise pas la taille de l2, mais est plutôt inférieur.

print(sys.getsizeof([None]))
72

Et c'est bien plus qu'un dixième de la taille de l'entreprise. l1 .

Vos chiffres devraient varier en fonction des détails de votre système d'exploitation et des détails de l'utilisation actuelle de la mémoire dans votre système d'exploitation. La taille de [None] ne peut jamais être supérieure à la mémoire adjacente disponible où la variable doit être stockée, et la variable peut devoir être déplacée si elle est allouée dynamiquement plus tard pour être plus grande.

1 votes

None n'est pas réellement stockée dans le tableau sous-jacent, la seule chose qui est stockée est un fichier PyObject pointeur (8 octets). Tous les objets Python sont alloués sur le tas. None est un singleton, donc avoir une liste avec beaucoup de non est simplement créer un tableau de PyObjects pointant sur le même None sur le tas (et ne pas utiliser de mémoire supplémentaire dans le processus pour chaque objet None ). Je ne suis pas sûr de ce que vous voulez dire par "None n'a pas de taille pré-spécifiée", mais cela ne semble pas correct. Enfin, votre boucle avec getsizeof chaque élément ne démontre pas ce que vous semblez croire qu'il démontre.

0 votes

Si ce que vous dites est vrai, la taille de [None]*10 devrait être la même que celle de [None]. Mais il est clair que ce n'est pas le cas - un stockage supplémentaire a été ajouté. En fait, la taille de [Aucun] répétée dix fois (160) est également inférieure à la taille de [Aucun] multipliée par dix. Comme vous le soulignez, il est clair que la taille du pointeur vers [None] est inférieure à la taille de [None] lui-même (16 octets au lieu de 72 octets). Cependant, 160+32 fait 192. Je ne pense pas non plus que la réponse précédente résolve entièrement le problème. Il est clair qu'une petite quantité supplémentaire de mémoire (qui dépend peut-être de l'état de la machine) est allouée.

0 votes

"Si ce que vous dites est vrai, la taille de [None]*10 devrait être la même que la taille de [None]" qu'est-ce que je dis qui pourrait impliquer cela ? Encore une fois, vous semblez vous concentrer sur le fait que le tampon sous-jacent est sur-alloué, ou que la taille de la liste inclut plus que la taille du tampon sous-jacent (c'est bien sûr le cas), mais ce n'est pas le but de cette question. Encore une fois, votre utilisation de gestsizeof sur chaque ele de l2 est trompeuse car getsizeof(l2) ne tient pas compte de la taille des éléments à l'intérieur du conteneur .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X