web-dev-qa-db-fra.com

list () utilise plus de mémoire que la compréhension de liste

Donc je jouais avec des objets list et j'ai trouvé une petite chose étrange que si list est créé avec list() il utilise plus de mémoire que la compréhension de liste? J'utilise Python 3.5.2

In [1]: import sys
In [2]: a = list(range(100))
In [3]: sys.getsizeof(a)
Out[3]: 1008
In [4]: b = [i for i in range(100)]
In [5]: sys.getsizeof(b)
Out[5]: 912
In [6]: type(a) == type(b)
Out[6]: True
In [7]: a == b
Out[7]: True
In [8]: sys.getsizeof(list(b))
Out[8]: 1008

De la docs :

Les listes peuvent être construites de plusieurs manières:

  • En utilisant une paire de crochets pour indiquer la liste vide: []
  • En utilisant des crochets, en séparant les éléments par des virgules: [a], [a, b, c]
  • Utilisation d'une compréhension de liste: [x for x in iterable]
  • Utilisation du constructeur de type: list() ou list(iterable)

Mais il semble qu'en utilisant list() il utilise plus de mémoire.

Et d'autant que list est plus grand, l'écart augmente.

Difference in memory

Pourquoi ça arrive?

MISE À JOUR # 1

Testez avec Python 3.6.0b2:

Python 3.6.0b2 (default, Oct 11 2016, 11:52:53) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(range(100)))
1008
>>> sys.getsizeof([i for i in range(100)])
912

MISE À JOUR # 2

Testez avec Python 2.7.12:

Python 2.7.12 (default, Jul  1 2016, 15:12:24) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getsizeof(list(xrange(100)))
1016
>>> sys.getsizeof([i for i in xrange(100)])
920
78
vishes_shell

Je pense que vous voyez des schémas de surallocation c'est un échantillon de la source :

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 */

new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

En imprimant les tailles des listes de compréhension de longueurs 0-88, vous pouvez voir les correspondances de motifs:

# create comprehensions for sizes 0-88
comprehensions = [sys.getsizeof([1 for _ in range(l)]) for l in range(90)]

# only take those that resulted in growth compared to previous length
steps = Zip(comprehensions, comprehensions[1:])
growths = [x for x in list(enumerate(steps)) if x[1][0] != x[1][1]]

# print the results:
for growth in growths:
    print(growth)

Résultats (le format est (list length, (old total size, new total size))):

(0, (64, 96)) 
(4, (96, 128))
(8, (128, 192))
(16, (192, 264))
(25, (264, 344))
(35, (344, 432))
(46, (432, 528))
(58, (528, 640))
(72, (640, 768))
(88, (768, 912))

La surallocation est effectuée pour des raisons de performances permettant aux listes de croître sans allouer plus de mémoire à chaque croissance (meilleure amorti performances).

Une raison probable de la différence avec l'utilisation de la compréhension de liste est que la compréhension de liste ne peut pas calculer de manière déterministe la taille de la liste générée, mais list() le peut. Cela signifie que les compréhensions augmenteront continuellement la liste au fur et à mesure qu'elle la remplira en utilisant une surallocation jusqu'à ce qu'elle soit finalement remplie.

Il est possible que cela n'augmente pas le tampon de surallocation avec les nœuds alloués inutilisés une fois terminé (en fait, dans la plupart des cas, cela ne va pas à l'encontre de l'objectif de surallocation).

list(), cependant, peut ajouter du tampon quelle que soit la taille de la liste car il connaît à l'avance la taille finale de la liste.


Un autre élément de preuve, également de la source, est que nous voyons liste des interprétations invoquant LIST_APPEND , qui indique l'utilisation de list.resize, Qui à son tour indique consommer la pré-allocation tampon sans savoir combien il sera rempli. Cela correspond au comportement que vous voyez.


Pour conclure, list() pré-allouera plus de nœuds en fonction de la taille de la liste

>>> sys.getsizeof(list([1,2,3]))
60
>>> sys.getsizeof(list([1,2,3,4]))
64

La compréhension de la liste ne connaît pas la taille de la liste, elle utilise donc les opérations d'ajout à mesure qu'elle grandit, ce qui épuise le tampon de pré-allocation:

# one item before filling pre-allocation buffer completely
>>> sys.getsizeof([i for i in [1,2,3]]) 
52
# fills pre-allocation buffer completely
# note that size did not change, we still have buffered unused nodes
>>> sys.getsizeof([i for i in [1,2,3,4]]) 
52
# grows pre-allocation buffer
>>> sys.getsizeof([i for i in [1,2,3,4,5]])
68
59
Reut Sharabani

Merci à tous de m'avoir aidé à comprendre ce génial Python.

Je ne veux pas poser une question aussi massive (c'est pourquoi je poste une réponse), je veux juste montrer et partager mes pensées.

Comme @ ReutSharabani l'a noté correctement: "list () détermine de manière déterministe la taille de la liste". Vous pouvez le voir sur ce graphique.

graph of sizes

Lorsque vous append ou que vous utilisez la compréhension de liste, vous avez toujours une sorte de frontière qui s'étend lorsque vous atteignez un certain point. Et avec list() vous avez presque les mêmes limites, mais elles flottent.

[~ # ~] mise à jour [~ # ~]

Merci donc à @ ReutSharabani , @ tavo , @ SvenFestersen

Pour résumer: list() pré-alloue la mémoire en fonction de la taille de la liste, la compréhension de la liste ne peut pas le faire (elle demande plus de mémoire quand elle en a besoin, comme .append()). C'est pourquoi list() stocke plus de mémoire.

Un autre graphique, qui montre list() préallouer la mémoire. La ligne verte montre donc list(range(830)) ajoutant élément par élément et pendant un moment la mémoire ne change pas.

list() preallocates memory

MISE À JOUR 2

Comme @Barmar l'a noté dans les commentaires ci-dessous, list() doit m'être plus rapide que la compréhension de la liste, j'ai donc exécuté timeit() avec number=1000 pour la longueur de list de 4**0 à 4**10 et les résultats sont

time measurements

28
vishes_shell