Warum ist 'new_file + = line + string' so viel schneller als 'new_file = new_file + line + string'? [Duplikat]

8

Unser Code benötigt 10 Minuten, um 68.000 Datensätze zu erfassen, wenn wir Folgendes verwenden:

%Vor%

Wenn wir jedoch Folgendes tun, dauert es nur 1 Sekunde:

%Vor%

Hier ist der Code:

%Vor%

Der gesamte Code, den ich jemals in Python geschrieben habe, verwendet die erste Option. Das sind nur grundlegende String-Operationen ... wir lesen die Eingabe aus einer Datei, verarbeiten sie und geben sie in die neue Datei aus. Ich bin 100% sicher, dass die erste Methode etwa 600 Mal länger dauert als die zweite, aber warum?

Die zu verarbeitende Datei ist ein CSV, verwendet aber ~ anstelle eines Kommas. Alles, was wir hier tun, nimmt diese csv, die eine Spalte für Land hat, und fügt eine Spalte für die Länderregion hinzu, z. LAC, EMEA, NA, etc ... cmdbre.regions ist nur ein Wörterbuch, mit allen ~ 200 Ländern als Schlüssel und jeder Region als Wert.

Sobald ich in die Append-String-Operation gewechselt habe ... wurde die Schleife in 1 Sekunde anstatt in 10 Minuten abgeschlossen ... 68.000 Datensätze in der CSV.

    
gunslingor 06.12.2016, 13:41
quelle

2 Antworten

22

CPython (der Referenzinterpreter) hat eine Optimierung für die direkte Kettenverkettung (wenn die anhängende Kette keine anderen Referenzen hat). Es kann diese Optimierung nicht so zuverlässig anwenden, wenn + gemacht wird, nur += ( + beinhaltet zwei Live-Referenzen, das Zuweisungsziel und den Operanden, und ersteres ist nicht an der Operation + beteiligt, also es ist schwieriger, es zu optimieren).

Sie sollten sich jedoch nicht darauf verlassen, nach PEP 8 :

  

Code sollte so geschrieben werden, dass andere Implementierungen von Python (PyPy, Jython, IronPython, Cython, Psyco und so weiter) nicht benachteiligt werden.

     

Verweisen Sie beispielsweise nicht auf CPythons effiziente Implementierung der In-Place-String-Verkettung für Anweisungen in der Form a + = b oder a = a + b. Diese Optimierung ist selbst in CPython fragil (sie funktioniert nur bei einigen Typen) und ist in Implementierungen, die Refcounting nicht verwenden, überhaupt nicht vorhanden. In leistungssensitiven Teilen der Bibliothek sollte stattdessen das Formular ".join ()" verwendet werden. Dadurch wird sichergestellt, dass die Verkettung über verschiedene Implementierungen in linearer Zeit erfolgt.

Aktualisierung basierend auf Fragenbearbeitungen : Ja, Sie haben die Optimierung abgebrochen. Sie verketteten viele Strings, nicht nur einen, und Python wertet von links nach rechts aus, also muss es zuerst die linkeste Verkettung machen. Also:

%Vor%

ist nicht das Gleiche wie:

%Vor%

weil das erstere alle neuen Stücke miteinander verkettet und sie dann an die Akkumulatorzeichenfolge auf einmal anfügt, während das letztere jede Addition von links nach rechts mit Provisorien auswerten muss, die nicht% enthalten co_de% selbst Hinzufügen von Parens für Klarheit, es ist wie du es getan hast:

%Vor%

Da es die Typen nicht wirklich kennt, bis sie sie erreichen, kann es nicht davon ausgehen, dass all diese Zeichenfolgen sind, so dass die Optimierung nicht einsetzt.

Wenn Sie das zweite Bit des Codes in:

geändert haben %Vor%

oder etwas langsamer, aber immer noch um ein Vielfaches schneller als Ihr langsamer Code, weil er die CPython-Optimierung beibehält:

%Vor%

Damit die Akkumulation für CPython offensichtlich war, würden Sie das Leistungsproblem beheben. Aber ehrlich gesagt, sollten Sie new_file_content immer verwenden, wenn Sie eine logische Append-Operation wie diese durchführen. += existiert aus einem bestimmten Grund und bietet sowohl dem Betreuer als auch dem Interpreter nützliche Informationen. Darüber hinaus ist es eine gute Übung soweit DRY geht; Warum sollten Sie die Variable zweimal benennen, wenn Sie es nicht brauchen?

Natürlich ist auch hier die Verwendung von += nach PEP8-Richtlinien schlecht. In den meisten Sprachen mit unveränderlichen Strings (einschließlich der meisten Nicht-CPython-Python-Interpreter) ist die wiederholte String-Verkettung eine Form von Schlemiel the Painters Algorithmus , der schwerwiegende Leistungsprobleme verursacht. Die richtige Lösung besteht darin, ein += der Zeichenfolgen zu erstellen, dann list sie alle auf einmal, z. B .:

%Vor%

ist normalerweise schneller, auch wenn die CPython-String-Verkettungsoptionen verfügbar sind, und wird auch bei Nicht-CPython-Python-Interpretern zuverlässig sein, da es eine veränderbare join verwendet, um Ergebnisse effizient zu akkumulieren, und dann list vorberechnen lässt die Gesamtlänge des Strings, weisen Sie den endgültigen String auf einmal zu (statt inkrementeller Größenanpassungen auf dem Weg) und füllen Sie ihn genau einmal auf.

Randnotiz: Für Ihren speziellen Fall sollten Sie sich nicht ansammeln oder verketten. Sie haben eine Eingabedatei und eine Ausgabedatei und können Zeile für Zeile verarbeiten. Jedes Mal, wenn Sie Dateiinhalte anhängen oder akkumulieren, schreiben Sie sie stattdessen einfach aus (ich habe den Code ein wenig für PEP8-Kompatibilität und andere geringfügige Stilverbesserungen aufgeräumt, während ich dabei war):

%Vor%

Implementierungsdetails Tieftauchen

Für diejenigen, die sich mit Implementierungsdetails befassen, ist die CPitcon-String-Concat-Optimierung im Byte-Code-Interpreter implementiert, nicht am ''.join -Typ selbst ( str macht zwar die Mutations-Optimierung, benötigt aber Hilfe vom Interpreter zu Reparieren Sie die Anzahl der Referenzen, so dass sie weiß, dass sie die Optimierung sicher verwenden kann, ohne Interpreter-Hilfe würden nur C-Erweiterungsmodule von dieser Optimierung profitieren.

Wenn der Interpreter erkennt, dass beide Operanden die Python-Ebene PyUnicode_Append type (auf der C-Ebene, in Python 3 wird es immer noch als str bezeichnet, ein Erbe von 2.x Tagen, das sich nicht zu ändern lohnt), ruft es eine spezielle PyUnicode -Funktion , die überprüft, ob die nächste Anweisung eine von drei grundlegenden unicode_concatenate -Anweisungen ist.Wenn dies der Fall ist und das Ziel dem linken Operanden entspricht, löscht es den Zielverweis, so dass STORE_* nur einen Verweis auf den Operanden anzeigt, sodass der optimierte Code für PyUnicode_Append mit einem einzelnen Verweis aufgerufen werden kann .

Dies bedeutet, dass Sie nicht nur die Optimierung unterbrechen können, indem Sie

tun %Vor%

Sie können es auch jederzeit brechen, wenn die fragliche Variable kein Name auf oberster Ebene (global, verschachtelt oder lokal) ist. Wenn du mit einem Objektattribut arbeitest, einem str index, einem list Wert usw., wird dir auch dict nicht helfen, es wird kein "simple += " angezeigt, also es löscht die Zielreferenz nicht und alle erhalten das ultraslow, nicht-in-place-Verhalten:

%Vor%

Es ist auch spezifisch für den STORE -Typ; In Python 2 hilft die Optimierung nicht mit str -Objekten, und in Python 3 hilft sie nicht mit unicode -Objekten, und in keiner Version optimiert sie für Unterklassen von bytes ; diese nehmen immer den langsamen Weg.

Im Grunde genommen ist die Optimierung in den einfachsten Fällen für Python-Neulinge so schön wie möglich, aber es wird selbst in etwas komplexeren Fällen nicht zu ernsthaften Schwierigkeiten kommen. Dies verstärkt nur die Empfehlung von PEP8: Je nach den Implementierungsdetails Ihres Interpreters ist es eine schlechte Idee, wenn Sie schneller auf jedem Interpreter für jedes Geschäftsziel laufen könnten, indem Sie das Richtige tun und str verwenden.

    
ShadowRanger 06.12.2016, 13:47
quelle
7

Eigentlich könnten beide gleichermaßen langsam sein, aber für einige Optimierungen, die eigentlich ein Implementierungsdetail der offiziellen Python-Laufzeit (cPython) sind.

Strings in Python sind unveränderlich - das heißt, wenn Sie "str1 + str2" machen, muss Python ein drittes string-Objekt erstellen und alle Inhalte von str1 und von str2 nach it kopieren - egal wie groß diese Teile sind.

Der inplace-Operator erlaubt Python, einige interne Optimierungen zu verwenden, so dass alle Daten in str1 nicht unbedingt erneut kopiert werden müssen - und wahrscheinlich sogar Pufferspeicher für weitere Verkettungsoptionen.

Wenn man das Gefühl hat, wie die Sprache funktioniert, besteht die Möglichkeit, einen großen Textkörper aus kleinen Strings aufzubauen, darin, eine Python-Liste mit allen Strings zu erstellen. co_de% Methode übergibt alle String-Komponenten. Das wird auch bei Python-Implementierungen konstant schnell sein und nicht von Optimierungen abhängig sein, um ausgelöst werden zu können.

%Vor%     
jsbueno 06.12.2016 13:49
quelle