Frage list () verwendet mehr Speicher als das Listenverständnis


Also ich habe mit gespielt list Objekte und kleine seltsame Sache gefunden, wenn list wird mit erstellt list() es verwendet mehr Speicher, als Listenverständnis? Ich benutze 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

Von dem Dokumente:

Listen können auf verschiedene Arten erstellt werden:

  • Verwenden Sie ein Paar eckiger Klammern, um die leere Liste zu bezeichnen: []
  • Verwenden Sie eckige Klammern, um Elemente durch Kommas zu trennen: [a], [a, b, c]
  • Verwenden eines Listenverständnisses: [x for x in iterable]
  • Verwenden des Typkonstruktors: list() oder list(iterable)

Aber es scheint, dass zu verwenden list() es verwendet mehr Speicher.

Und so viel list ist größer, der Abstand erhöht sich.

Difference in memory

Warum passiert das?

UPDATE # 1

Testen Sie mit 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

UPDATE # 2

Test mit 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

75
2017-10-13 10:25


Ursprung


Antworten:


Ich denke, Sie sehen Überverteilungsmuster, das ist ein Probe von der Quelle:

/* 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);

Wenn Sie die Größen der Listenkompressentierungen der Längen 0-88 drucken, sehen Sie, dass das Muster übereinstimmt:

# 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)

Ergebnisse (Format ist (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))

Die Überallokation erfolgt aus Performance-Gründen, damit die Listen wachsen können, ohne bei jedem Wachstum mehr Speicher zuzuweisen (besser amortisiert Performance).

Ein möglicher Grund für den Unterschied zur Verwendung des Listenverständnisses ist, dass das Listenverständnis die Größe der generierten Liste nicht deterministisch berechnen kann, sondern list() kann. Das bedeutet, dass die Comprehensions die Liste kontinuierlich erweitern werden, indem sie die Überallokation füllt, bis sie schließlich gefüllt wird.

Es ist möglich, dass der Überzuweisungspuffer nicht mit nicht verwendeten zugewiesenen Knoten wächst, wenn er einmal fertig ist (tatsächlich würde dies in den meisten Fällen den Zweck der Überzuteilung nicht erfüllen).

list()kann jedoch einen Puffer hinzufügen, unabhängig von der Listengröße, da er die endgültige Listengröße im Voraus kennt.


Ein weiterer Beweis dafür, auch von der Quelle, ist, dass wir sehen list comprehensions aufrufen LIST_APPEND, die die Verwendung von angibt list.resize, was wiederum anzeigt, dass der Vorbelegungspuffer verbraucht wird, ohne zu wissen, wie viel davon gefüllt wird. Dies entspricht dem Verhalten, das Sie sehen.


Schlussfolgern, list() wird mehr Knoten als eine Funktion der Listengröße vorbelegen

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

Listenverstehen kennt die Listengröße nicht, daher verwendet sie Append-Operationen, wenn sie wächst, wodurch der Vorbelegungspuffer aufgebraucht wird:

# 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

55
2017-10-13 10:40



Danke an alle, die mir geholfen haben, diesen tollen Python zu verstehen.

Ich möchte diese Frage nicht so massiv stellen (das ist der Grund warum ich eine Antwort schreibe), ich möchte nur meine Gedanken zeigen und teilen.

Wie @ ReutSharabani richtig beachtet: "list () bestimmt deterministisch Listengröße". Sie können es von diesem Diagramm sehen.

graph of sizes

Wenn du append oder mit Listenverständnis haben Sie immer eine Art von Grenzen, die sich ausdehnen, wenn Sie einen Punkt erreichen. Und mit list() Sie haben fast dieselben Grenzen, aber sie schweben.

AKTUALISIEREN

Also danke @ ReutSharabani, @ tavo, @SvenFestersen

Um zusammenzufassen: list() preallocates Speicher hängt von der Listengröße ab, Listenverstehen kann das nicht (es erfordert mehr Speicher, wenn es benötigt wird, wie .append()). Deshalb list() mehr Speicher speichern.

Noch ein Diagramm, das zeigt list() Speicher vorab reservieren. So grüne Linie zeigt list(range(830)) Anhängen Element für Element und für eine Weile Speicher nicht ändern.

list() preallocates memory

UPDATE 2

Wie @Barmar in den Kommentaren unten bemerkt, list() Muss ich schneller als Listenverständnis, also lief ich timeit() mit number=1000 für die Länge von list von 4**0 zu 4**10 und die Ergebnisse sind

time measurements


27
2017-10-13 11:37