it-swarm.com.de

Gibt es eine Möglichkeit, Python list.append () zu umgehen, die in einer Schleife mit zunehmender Liste immer langsamer wird?

Ich habe eine große Datei, aus der ich lese, und konvertiere alle paar Zeilen in eine Instanz eines Objekts.

Da ich die Datei durchläuft, verwahre ich die Instanz mit list.append (Instanz) in einer Liste und fahre dann mit der Schleife fort.

Dies ist eine Datei mit ~ 100 MB, also nicht zu groß, aber wenn die Liste größer wird, verlangsamt sich das Looping progressiv. (Ich drucke die Zeit für jede Runde in der Schleife).

Dies ist für die Schleife nicht unabdingbar. Wenn ich jede neue Instanz drucke, während ich die Datei durchlaufe, wird das Programm mit konstanter Geschwindigkeit ausgeführt. Nur wenn ich sie an eine Liste anhänge, wird es langsam.

Mein Freund schlug vor, die Garbage Collection vor der while-Schleife zu deaktivieren und danach zu aktivieren und einen Garbage Collection-Anruf durchzuführen.

Hat jemand anderes ein ähnliches Problem mit list.append festgestellt, langsamer zu werden? Gibt es eine andere Möglichkeit, dies zu umgehen?


Ich werde die folgenden zwei Dinge ausprobieren, die unten vorgeschlagen werden.

(1) "Vorabzuordnen" des Speichers ~ Was ist der beste Weg, dies zu tun? (2) Versuchen Sie es mit Deque

Mehrere Beiträge (siehe Kommentar von Alex Martelli) deuteten auf eine Fragmentierung des Gedächtnisses hin (er verfügt über eine große Menge an verfügbarem Arbeitsspeicher wie ich), jedoch keine offensichtlichen Korrekturen für die Leistung.

Um das Phänomen zu replizieren, führen Sie den in den Antworten angegebenen Testcode aus und nehmen Sie an, dass die Listen nützliche Daten enthalten.


gc.disable () und gc.enable () helfen beim Timing. Ich werde auch sorgfältig analysieren, wo die ganze Zeit verbracht wird.

50
Deniz

Die von Ihnen beobachtete schlechte Leistung wird durch einen Fehler im Python-Garbage-Collector in der verwendeten Version verursacht. Führen Sie ein Upgrade auf Python 2.7 oder 3.1 oder höher durch, um das amordensierte 0(1) - Verhalten zu erhalten, das erwartet wird, wenn Listen in Python angehängt werden.

Wenn Sie kein Upgrade durchführen können, deaktivieren Sie die Speicherbereinigung beim Erstellen der Liste und aktivieren Sie sie, wenn Sie fertig sind. 

(Sie können auch die Auslöser des Speicherbereinigers anpassen oder "collect" während des Fortschritts selektiv aufrufen, aber ich untersuche diese Optionen nicht, da diese komplexer sind und ich vermute, dass Ihr Anwendungsfall für die obige Lösung geeignet ist.)

Hintergrund:

Siehe: https://bugs.python.org/issue4074 und auch https://docs.python.org/release/2.5.2/lib/module-gc.html

Der Reporter beobachtet, dass das Anhängen komplexer Objekte (Objekte, die keine Zahlen oder Strings sind) an eine Liste linear verlangsamt wird, wenn die Liste länger wird.

Der Grund für dieses Verhalten ist, dass der Speicherbereiniger jedes Objekt in der Liste überprüft und erneut prüft, ob sie für die Speicherbereinigung geeignet sind. Dieses Verhalten bewirkt, dass die lineare Zeit länger wird, um Objekte zu einer Liste hinzuzufügen. Es wird erwartet, dass ein Fix in py3k landet, daher sollte es nicht für den von Ihnen verwendeten Interpreter gelten.

Prüfung:

Ich habe einen Test durchgeführt, um dies zu demonstrieren. Für 1k-Iterationen füge ich 10k-Objekte an eine Liste an und zeichne die Laufzeit für jede Iteration auf. Der Gesamtlaufzeitunterschied ist sofort ersichtlich. Wenn die Garbage Collection während der inneren Schleife des Tests deaktiviert ist, beträgt die Laufzeit auf meinem System 18,6 Sekunden. Wenn die Garbage Collection für den gesamten Test aktiviert ist, beträgt die Laufzeit 899,4 Sekunden.

Dies ist der Test:

import time
import gc

class A:
    def __init__(self):
        self.x = 1
        self.y = 2
        self.why = 'no reason'

def time_to_append(size, append_list, item_gen):
    t0 = time.time()
    for i in xrange(0, size):
        append_list.append(item_gen())
    return time.time() - t0

def test():
    x = []
    count = 10000
    for i in xrange(0,1000):
        print len(x), time_to_append(count, x, lambda: A())

def test_nogc():
    x = []
    count = 10000
    for i in xrange(0,1000):
        gc.disable()
        print len(x), time_to_append(count, x, lambda: A())
        gc.enable()

Vollständige Quelle: https://hypervolu.me/~erik/programming/python_lists/listtest.py.txt

Grafisches Ergebnis: Rot ist bei gc an, Blau ist bei gc aus. Die y-Achse ist logarithmisch in Sekunden skaliert.

http://hypervolu.me/~erik/programming/python_lists/gc.png

Da sich die beiden Darstellungen in der y-Komponente um mehrere Größenordnungen unterscheiden, sind sie hier unabhängig voneinander, wobei die y-Achse linear skaliert ist.

http://hypervolu.me/~erik/programming/python_lists/gc_on.png

http://hypervolu.me/~erik/programming/python_lists/gc_off.png

Wenn die Speicherbereinigung deaktiviert ist, sehen wir interessanterweise nur geringe Spitzen in der Laufzeit pro 10-k-Anhänge, was darauf hindeutet, dass die Neuzuordnungskosten von Python relativ niedrig sind. In jedem Fall sind sie um viele Größenordnungen niedriger als die Müllsammelkosten.

Die Dichte der obigen Diagramme macht es schwierig zu erkennen, dass die meisten Intervalle bei eingeschaltetem Müllsammler tatsächlich eine gute Leistung zeigen; Nur wenn der Speicherbereinigungszyklus durchläuft, stoßen wir auf das pathologische Verhalten. Sie können dies in diesem Histogramm der Anhängezeit von 10k beobachten. Die meisten Datenpunkte fallen um 0,02s pro 10k-Anhänge.

http://hypervolu.me/~erik/programming/python_lists/gc_on.hist.png

Die für die Erstellung dieser Diagramme verwendeten Rohdaten finden Sie unter http://hypervolu.me/~erik/programming/python_lists/

91
Erik Garrison

Es gibt nichts zu umgehen: An eine Liste angehängt wird O(1) amortisiert.  

Eine Liste (in CPython) ist ein Array, mindestens so lange wie die Liste und bis zu doppelt so lang. Wenn das Array nicht voll ist, ist das Anhängen an eine Liste genauso einfach wie das Zuordnen eines der Arraymitglieder (O (1)). Jedes Mal, wenn das Array voll ist, wird es automatisch in der Größe verdoppelt. Dies bedeutet, dass gelegentlich eine O(n) - Operation erforderlich ist, aber es wird nur alle n Operationen benötigt, und es wird immer seltener, da die Liste groß wird. O(n)/n ==> O (1). (In anderen Implementierungen können sich die Namen und Details möglicherweise ändern, die Eigenschaften müssen jedoch gleichzeitig beibehalten werden.)

Das Anhängen an eine Liste ist bereits skaliert.

Kann es sein, dass Sie, wenn die Datei groß wird, nicht alles im Speicher halten können und Sie Probleme mit dem Betriebssystem-Paging auf die Festplatte haben? Ist es möglich, dass es sich um einen anderen Teil Ihres Algorithmus handelt, der nicht gut skalierbar ist?

13
Mike Graham

Viele dieser Antworten sind nur wilde Vermutungen. Mike Graham gefällt mir am besten, weil er recht hat, wie Listen implementiert werden. Ich habe jedoch einen Code geschrieben, um Ihre Behauptung zu reproduzieren und weiter zu untersuchen. Hier sind einige Erkenntnisse.

Hier habe ich angefangen.

import time
x = []
for i in range(100):
    start = time.clock()
    for j in range(100000):
        x.append([])
    end = time.clock()
    print end - start

Ich füge gerade leere Listen an die Liste x an. Ich drucke 100 Mal pro 100.000 Anhänge eine Dauer aus. Es verlangsamt sich, wie Sie behaupteten. (0,03 Sekunden für die erste Iteration und 0,84 Sekunden für die letzte ... ein großer Unterschied.)

Wenn Sie eine Liste instanziieren, sie jedoch nicht an x anhängen, wird sie natürlich schneller ausgeführt und im Laufe der Zeit nicht vergrößert.

Wenn Sie jedoch x.append([]) in x.append('hello world') ändern, wird die Geschwindigkeit überhaupt nicht erhöht. Das gleiche Objekt wird 100 * 100.000-mal zur Liste hinzugefügt.

Was ich daraus mache:

  • Die Geschwindigkeitsreduzierung hat nichts mit der Größe der Liste zu tun. Dies hat mit der Anzahl der Live-Python-Objekte zu tun.
  • Wenn Sie die Elemente überhaupt nicht an die Liste anhängen, wird der Müll sofort gesammelt und nicht mehr von Python verwaltet.
  • Wenn Sie dasselbe Element immer wieder anfügen, steigt die Anzahl der Live-Python-Objekte nicht an. Aber die Liste muss sich von Zeit zu Zeit ändern. Dies ist jedoch nicht die Quelle des Leistungsproblems.
  • Da Sie viele neu erstellte Objekte erstellen und zu einer Liste hinzufügen, bleiben sie live und werden nicht als Müll gesammelt. Die Verlangsamung hat wahrscheinlich etwas damit zu tun.

Was die inneren Elemente von Python angeht, die das erklären könnten, bin ich mir nicht sicher. Aber ich bin mir ziemlich sicher, dass die Listendatenstruktur nicht der Täter ist.

6
FogleBird

Dieses Problem ist bei der Verwendung von Numpy-Arrays aufgetreten, die wie folgt erstellt wurden:

import numpy
theArray = array([],dtype='int32')

Das Anhängen an dieses Array innerhalb einer Schleife dauerte mit dem Wachstum des Arrays zunehmend länger, was angesichts der Tatsache, dass ich 14 Millionen Anhänge hatte, ein Dealbreaker war.

Die oben beschriebene Speicherbereinigungslösung klang vielversprechend, funktionierte jedoch nicht.

Was funktionierte, war das Erstellen des Arrays mit einer vordefinierten Größe wie folgt:

theArray = array(arange(limit),dtype='int32')

Stellen Sie nur sicher, dass limit größer ist als das benötigte Array.

Sie können dann jedes Element im Array direkt festlegen:

theArray[i] = val_i

Und am Ende können Sie bei Bedarf den nicht verwendeten Teil des Arrays entfernen

theArray = theArray[:i]

Dies machte in meinem Fall einen RIESEN Unterschied.

1
Nathan Labenz

Kannst du es versuchen http://docs.python.org/release/2.5.2/lib/deque-objects.html Zuordnen der erwarteten Anzahl erforderlicher Elemente in Ihrer Liste? ? Ich würde wetten, dass die Liste ein zusammenhängender Speicher ist, der alle paar Iterationen neu zugewiesen und kopiert werden muss.

EDIT: Gesichert durch http://www.python.org/doc/faq/general/#how-are-lists-implemented

1

Verwenden Sie stattdessen einen Satz und konvertieren Sie ihn am Ende in eine Liste

my_set=set()
with open(in_file) as f:
    # do your thing
    my_set.add(instance)


my_list=list(my_set)
my_list.sort() # if you want it sorted

Ich hatte das gleiche Problem und dieses löste das Zeitproblem durch mehrere Bestellungen. 

0
Kahiga