list () verbraucht etwas mehr Speicher als das Listenverständnis

79

Also habe ich mit listObjekten gespielt und wenig Seltsames gefunden, das, wenn listes damit erstellt list()wird, mehr Speicher benötigt 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

Aus den Dokumenten :

Listen können auf verschiedene Arten erstellt werden:

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

Aber es scheint, dass die list()Verwendung mehr Speicher benötigt.

Und je viel listgrößer ist, desto größer wird der Abstand.

Unterschied im Gedächtnis

Warum passiert das?

UPDATE # 1

Test 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
vishes_shell
quelle
3
Das ist eine sehr interessante Frage. Ich kann das Phänomen in Python 3.4.3 reproduzieren. Noch interessanter: Auf Python 2.7.5 sys.getsizeof(list(range(100)))ist 1016, getsizeof(range(100))ist 872 und getsizeof([i for i in range(100)])ist 920. Alle haben den Typ list.
Sven Festersen
Interessant ist, dass dieser Unterschied auch in Python 2.7.10 vorhanden ist (obwohl sich die tatsächlichen Zahlen von Python 3 unterscheiden). Auch dort in 3.5 und 3.6b.
Cdarke
Ich erhalte für Python 2.7.6 die gleichen Zahlen wie für @SvenFestersen, auch bei Verwendung xrange.
RemcoGerlich
2
Hier gibt es eine mögliche Erklärung: stackoverflow.com/questions/7247298/size-of-list-in-memory . Wenn eine der Methoden die Liste mit erstellt append(), liegt möglicherweise eine Überbelegung des Speichers vor. Ich denke, der einzige Weg, dies wirklich zu klären, besteht darin, sich die Python-Quellen anzusehen.
Sven Festersen
Nur 10% mehr (das sagt man nirgendwo wirklich). Ich würde den Titel in "etwas mehr" umformulieren.
smci

Antworten:

61

Ich denke, Sie sehen Überzuordnungsmuster. Dies ist ein Beispiel aus 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 des Listenverständnisses der Längen 0-88 drucken, sehen Sie die Musterübereinstimmungen:

# 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 Überzuweisung erfolgt aus Leistungsgründen, sodass Listen wachsen können, ohne mit jedem Wachstum mehr Speicher zuzuweisen (bessere amortisierte Leistung).

Ein wahrscheinlicher Grund für den Unterschied bei der Verwendung des Listenverständnisses ist, dass das Listenverständnis die Größe der generierten Liste nicht deterministisch berechnen list()kann , sondern kann. Dies bedeutet, dass das Verständnis die Liste kontinuierlich erweitert, während es mit Überzuweisung gefüllt wird, bis es schließlich gefüllt wird.

Es ist möglich, dass der Überzuweisungspuffer mit nicht verwendeten zugewiesenen Knoten nicht vergrößert wird, sobald dies erledigt ist (in den meisten Fällen wird dies den Zweck der Überzuweisung nicht zunichte machen).

list()Es kann jedoch unabhängig von der Listengröße ein Puffer hinzugefügt werden, da die endgültige Listengröße im Voraus bekannt ist.


Ein weiterer Beleg, ebenfalls aus der Quelle, ist, dass Listenverständnisse aufgerufen werdenLIST_APPEND , die auf die Verwendung von hinweisen list.resize, was wiederum darauf hinweist, dass der Puffer vor der Zuweisung verbraucht wird, ohne zu wissen, wie viel davon gefüllt wird. Dies stimmt mit dem Verhalten überein, das Sie sehen.


Abschließend list()wird mehr Knoten in Abhängigkeit von der Liste Größe vorbelegt

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

Das Listenverständnis kennt die Listengröße nicht und verwendet daher Anhängeoperationen, wenn es wächst, wodurch der Puffer für die Vorzuweisung 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
Reut Sharabani
quelle
4
Aber warum sollte die Überallokation mit dem einen, aber nicht mit dem anderen geschehen?
Cdarke
Dies ist speziell von list.resize. Ich bin kein Experte für die Navigation durch die Quelle, aber wenn einer die Größe ändert und der andere nicht, könnte dies den Unterschied erklären.
Reut Sharabani
6
Python 3.5.2 hier. Versuchen Sie, Listengrößen von 0 bis 35 in einer Schleife zu drucken. Zur Liste sehe ich 64, 96, 104, 112, 120, 128, 136, 144, 160, 192, 200, 208, 216, 224, 232, 240, 256, 264, 272, 280, 288, 296, 304, 312, 328, 336, 344, 352, 360, 368, 376, 384, 400, 408, 416und zum Verständnis 64, 96, 96, 96, 96, 128, 128, 128, 128, 192, 192, 192, 192, 192, 192, 192, 192, 264, 264, 264, 264, 264, 264, 264, 264, 264, 344, 344, 344, 344, 344, 344, 344, 344, 344. Ich würde davon ausgehen, dass dieses Verständnis derjenige ist, der den Speicher als Algorithmus zuzuweisen scheint, der für bestimmte Größen mehr RAM verwendet.
Tavo
Ich würde das gleiche erwarten. Ich kann es mir bald genauer ansehen. Gute Kommentare.
Reut Sharabani
4
tatsächlich list()bestimmt determinisListenGröße, die Liste Verständnis nicht tun kann. Dies deutet darauf hin, dass das Listenverständnis nicht immer das "letzte" Wachstum der Liste "auslöst". Könnte Sinn machen.
Reut Sharabani
30

Vielen Dank an alle, die mir geholfen haben, dieses großartige Python zu verstehen.

Ich möchte keine so massiven Fragen stellen (deshalb poste ich eine Antwort), sondern nur meine Gedanken zeigen und teilen.

Wie @ReutSharabani richtig bemerkte: "list () bestimmt deterministisch die Listengröße ". Sie können es aus dieser Grafik sehen.

Diagramm der Größen

Wenn Sie appendoder Listenverständnis verwenden, haben Sie immer eine Art von Grenzen, die sich erweitern, wenn Sie einen Punkt erreichen. Und mit list()dir haben fast die gleichen Grenzen, aber sie schweben.

AKTUALISIEREN

Also danke an @ReutSharabani , @tavo , @SvenFestersen

Zusammenfassend list()lässt sich sagen, dass die Vorbelegung des Speichers von der Listengröße abhängt. Das Listenverständnis kann dies nicht (es fordert bei Bedarf mehr Speicher an .append()). Deshalb list()Speicher mehr Speicher.

Ein weiteres Diagramm, das die list()Vorbelegung des Speichers zeigt. Die grüne Linie zeigt also das list(range(830))Anhängen von Element zu Element und für eine Weile ändert sich der Speicher nicht.

list () ordnet den Speicher vorab zu

UPDATE 2

Wie @Barmar in den Kommentaren unten bemerkte, list()muss ich schneller als das Listenverständnis sein, also bin ich timeit()mit number=1000einer Länge von listvon 4**0bis gelaufen 4**10und die Ergebnisse sind

Zeitmessungen

vishes_shell
quelle
1
Die Antwort, warum die rote Linie über der blauen steht, lautet, dass der listKonstruktor , wenn er die Größe der neuen Liste anhand seines Arguments bestimmen kann, immer noch dieselbe Menge an Speicherplatz zuweist, als wenn das letzte Element gerade dort angekommen wäre und nicht genügend Speicherplatz dafür vorhanden wäre. Zumindest macht das für mich Sinn.
Tavo
@tavo es scheint mir das gleiche, nach einem Moment möchte ich es in der Grafik zeigen.
vishes_shell
2
Während Listenverständnisse weniger Speicher benötigen, sind sie aufgrund aller auftretenden Größenänderungen wahrscheinlich erheblich langsamer. Diese müssen häufig das Listen-Backbone in einen neuen Speicherbereich kopieren.
Barmar
@Barmar Eigentlich kann ich einige Zeitmessungen mit rangeObjekt durchführen (das könnte Spaß machen).
vishes_shell
Und es macht Ihre Grafiken noch schöner. :)
Barmar