Einschränkungen der Python-Vektorisierung als Leistungsmethode / Sudo Null IT News

Itamar Turner-Trauring

Autor des Sciagraph-Python-Profilers

Die NumPy-Vektorisierung beschleunigt Massendatenoperationen mit schnellem Low-Level-Code. Basierend auf NumPy ist Pandas auch so geschrieben, dass es die gleiche schnelle Funktionalität bietet. Aber die Vektorisierung ist kein Allheilmittel. Manchmal wird das Problem auf Kosten des Speichers gelöst, die gewünschte Operation wird nicht unterstützt oder einfach nicht benötigt. Jedes dieser Probleme hat unterschiedliche Lösungen.

Das Wort “Vektorisierung” hat mehrere Bedeutungen, ich werde über Schleifen auf niedriger Ebene sprechen

Zusammenfassen ausführliche Erklärung der Vektorisierung, im Kontext von Python hat Vektorisierung drei Bedeutungen. Das:

  1. API für die Arbeit mit massiven Daten: Der Code arr += 1 fügt beispielsweise jedem Element eines NumPy-Arrays 1 hinzu.

  2. Low-Level-API, zum Beispiel in C oder Rust. Es funktioniert schnell mit einer großen Datenmenge – das ist das Hauptthema des Artikels.

  3. SIMD-Anweisungen, die Low-Level-Operationen noch mehr beschleunigen. Details – ein Grundlegende Beschreibung der Vektorisierung.

Nehmen wir zunächst an, dass wir mit dem Standard-Python, also CPython, arbeiten.

Beispiel: Hinzufügen einer Zahl zu ganzen Zahlen in einer Liste aus Zeit Import Zeit l = Liste (Bereich (100_000_000)) Start = Zeit () für i in Bereich (Länge (l)): l[i] += 17 print(“Elapsed: “, time() – start)

Eine Liste von Python-Ganzzahlen in CPython ist eine Reihe von Zeigern auf ziemlich große Objekte. Aus Sicht von CPython sind dies gewöhnliche Python-Objekte, und das Hinzufügen einer Ganzzahl zu jedem Element der Liste besteht aus den folgenden Schritten:

  1. Ermitteln des Typs jedes Elements in der Liste.

  2. Finden einer Additionsfunktion für einen bestimmten Zahlentyp.

  3. Aufruf der gefundenen Funktion mit den ursprünglichen und hinzugefügten Objekten.

  4. Konvertieren Sie beide Python-Objekte in native Ganzzahlen.

  5. Ergänzung von Maschinennummern.

  6. Verpacken der resultierenden Ganzzahl in ein Python-Objekt.

  7. Das Finden des nächsten Elements in einer Python-Liste, was an sich schon eine teure Operation ist, beinhaltet das Finden der Iteratormethoden für den Bereichstyp, dann das Finden der Indizierungsoperation für den Listentyp, das Konvertieren des Indexobjekts in eine Maschinen-Ganzzahl … Viele Arbeit!

Im Gegensatz dazu ist ein NumPy-Array aus ganzen Zahlen Array von Maschinen-Ganzzahlen. Das Hinzufügen einer Ganzzahl zu jedem Element ist also nur ein paar Prozessorbefehle pro Element; Einmal initialisiert, unterscheidet es sich nicht vom C-Äquivalent.

Hier ist der NumPy-Code:

from time import time import numpy as np l = np.array(range(100_000_000), dtype=np.uint64) start = time() l += 17 print(“Elapsed: “, time() – start)

NumPy ist viel schneller:

Implementierung

Verstrichen (Sek.)

CPython

6.13

taub

0,07

Einschränkungen bei der Vektorisierung

Die Vektorisierung beschleunigt den Code, was großartig ist … aber es ist nicht perfekt. Hier ist das erste Problem:

Große nutzlose Speicherzuweisungen

Angenommen, wir möchten den durchschnittlichen Abstand der Elemente eines Arrays von der Zahl 0 wissen. Eine Möglichkeit, dies zu tun, besteht darin, den absoluten Wert jedes Elements zu berechnen und den Durchschnitt der Elemente zu nehmen:

imoprt numpy as np def mean_distance_from_zero(arr): gebe np.abs(arr).mean() zurück

Das funktioniert und beide Berechnungen sind schnell, aber es wird ein völlig neues Zwischenarray von Absolutwerten benötigt. Wenn das ursprüngliche Array 1000 MB enthält, werden weitere 1000 MB vorhanden sein. Das ursprüngliche Array kann überschrieben werden, um Speicherplatz zu sparen, aber es kann auch aus anderen Gründen erforderlich sein.

Um nur eine zusätzliche Kopie anstelle von N Kopien zu haben, können Sie verwenden Betrieb vor Ort. Es gibt Bibliotheken wie numexpr und Dask-Array, die in der Lage ist, Batch- oder intelligentere In-Place-Updates mit reduzierter Speicherauslastung durchzuführen. Oder Sie können einfach alles Element für Element berechnen, mit Ihrer eigenen Schleife, aber das führt zum zweiten Problem:

Nur was gestützt wird, beschleunigt

Damit die Vektorisierung funktioniert, benötigen Sie Maschinencode auf niedriger Ebene, der die Operation ausführt und die Schleife über die Daten verwaltet. Beim Zurückschalten auf die Schleifen und Funktionalität von regulärem Python geht die Geschwindigkeit verloren. Eine offensichtliche Möglichkeit, mean_distance_from_zero() zu implementieren, ohne den Speicherverbrauch zu erhöhen, ist beispielsweise wie folgt:

def mean_distance_from_zero(arr): total = 0 für i in range(len(arr)) total += abs(arr[i]) Gesamtsumme / Länge (arr) zurückgeben

Aber wir sind wieder bei der Schleife und den Operationen in Python, der Code ist wieder langsam – und das ist einer der Gründe, warum NumPy so viel selbst implementieren muss: Der Code wird jedes Mal langsamer, wenn Sie zu Python zurückkehren. In einer kompilierten Sprache besteht die Möglichkeit, dass der Compiler einen Weg findet, die zweite Implementierung zu optimieren, und dies führt zu einer Beschleunigung, die mit einer Reihe separater vektorisierter Operationen nicht möglich ist.

Die Vektorisierung beschleunigt nur Massenoperationen

Manchmal ist Ihr Code langsam, weil er die gleiche Operation für viele Datenelemente des gleichen Typs durchführt – hier ist die Vektorisierung praktisch. In anderen Fällen ist die Berechnung aus einem anderen Grund zu langsam, sodass eine Vektorisierung nicht hilft.

Andere Lösungen

Schauen wir uns die drei wichtigsten an:

  1. Beschleunigter Python-Interpreter, PyPy – Die CPython-Implementierung ist viel intelligenter: Sie kann die JIT-Kompilierung verwenden und die Ausführung beschleunigen, indem sie spezialisierten Maschinencode generiert.

  2. Native Codegenerierung in CPython via Numba.

  3. Kompilieren von benutzerdefiniertem Code mit Cython, C, C++ oder Rust.

PyPy: Ein beschleunigter Python-Interpreter

Mit einer Python-Liste von Ganzzahlen macht CPython nicht wirklich etwas Besonderes, weshalb NumPy-Arrays benötigt werden. PyPy ist hier intelligenter als CPython: Es generiert speziellen Code für diesen Fall.

Gehen wir zurück zum ursprünglichen Beispiel:

from time import time l = list(range(100_000_000)) start = time() for i in range(len(l)): l[i] += 17 print(“Elapsed: “, time() – start)

Und vergleiche Cython mit PyPy:

$ python add_list.py abgelaufen: 6.125197410583496 $ pypy add_list.py abgelaufen: 0.11461925506591797

Die Geschwindigkeit von PyPy ist vergleichbar mit der Geschwindigkeit von NumPy – und das ist eine normale Python-Schleife.

Leider spielt PyPy nicht besonders gut mit NumPy; Eine naive, nicht vektorisierte Schleife über ein NumPy-Array (wie das oben implementierte mean()) ist in PyPy viel langsamer als in CPython. Daher ist PyPy nutzlos, wenn es darum geht, mit der fehlenden Funktionalität von NumPy fertig zu werden, und es wird sogar eine langsame, nicht vektorisierte Schleife noch mehr verlangsamen. Wenn Sie jedoch mit Code auf Standard-Python-Objekten arbeiten, kann PyPy viel schneller sein als CPython, daher eignet es sich am besten zum Beschleunigen, wenn NumPy oder Pandas überhaupt nicht verwendet werden.

Numba: JIT-Funktionen, die mit NumPy funktionieren

Numba führt auch die JIT-Kompilierung durch, fungiert aber im Gegensatz zu PyPy als Add-on zu CPython und ist für die Zusammenarbeit mit NumPy ausgelegt. Mal sehen, wie das unsere Probleme löst:

NumPy-Erweiterung über Numba

Fehlende Operationen sind für Numba kein Problem: Schreiben Sie einfach Ihre eigene Funktion. Zum Beispiel mean_distance_from_zero() in reinem Python und Numba:

from time import time import numpy as np from numba import njit # Reine Python-Version: def mean_distance_from_zero(arr): total = 0 for i in range(len(arr)) total += abs(arr[i]) return total / len(arr) # Eine schnelle JIT-Version: mean_distance_from_zero_numba = njit( mean_distance_from_zero ) arr = np.array(range(10_000_000), dtype=np.float64) start = time() mean_distance_from_zero(arr) print(” Elapsed CPython: “, time() – start) for i in range(3): start = time() mean_distance_from_zero_numba(arr) print(“Elapsed Numba: “, time() – start)

Der erste Aufruf von Numba ist langsam: Die Bibliothek muss benutzerdefinierten Maschinencode kompilieren. Danach beschleunigen sich die Aufrufe und CPython ist Numba deutlich unterlegen:

$ python cpython_vs_numba.py

Da wir mit Numba eigene Zusatzfunktionen für NumPy schreiben können, sind die fehlenden Operationen kein Problem. Dies bedeutet auch, dass NumPy aufgrund der Einschränkungen der verfügbaren Arrays keine temporären Arrays erstellen muss, sodass Numba bei der Lösung der ersten beiden Probleme helfen kann, auf die wir gestoßen sind.

Numba ist bei nicht vektorisierten Operationen nicht so nützlich, da sie nicht Teil des Projektumfangs sind.

Kompilierter Cython-Code (oder Rust, C, C++)

Wir haben zwei Beispiele für die JIT-Kompilierung gesehen: einen vollständigen JIT-Interpreter und einzelne JIT-Funktionen. Die dritte Möglichkeit zur Beschleunigung ist die Vorkompilierung. Mit Cython, Rust oder C können Sie:

  1. Erstellen Sie schnelle Operationen für NumPy via Cythons integrierte NumPy-Unterstützungrostig in Rust oder eben C-API Python. NumPy selbst ist hauptsächlich in C geschrieben, während seine Erweiterungen in anderen Sprachen wie Fortran oder C++ geschrieben sind.

  2. Für Fälle ohne Vektorisierung, um schnelle Erweiterungen zu schreiben, kann man das wieder Verwenden Sie diese Sprachen.

Aber um diese Erweiterungen vor dem Ausführen von Python zu kompilieren, müssen Sie viele Verpackungskonfigurationseinstellungen hinzufügen oder den Build optimieren. Cython kann beim Import kompilieren, aber das macht es schwieriger, Ihre Software zu verteilen: Die Benutzer benötigen einen Compiler. Numba hat keine solchen Einschränkungen.

Lösungsauswahl

PyPy hilft bei einer vektorisierten Operation nicht. Obwohl Projekte wie HP-Projekt.

Bleiben Numba und kompilierte Erweiterungen.

  • Sie müssen im Voraus kompilieren, was einen Compiler und eine komplexe Paketierung erfordert, daher ist es normalerweise einfacher, Numba einzurichten.

  • Numba erstellt ohne zusätzliche Arbeit verschiedene Versionen des Codes für verschiedene Typen: Ganzzahlen oder Gleitkommazahlen; Bei kompiliertem Code müssen Sie für jeden Nummerntyp eine Version kompilieren und möglicherweise auch Code für jeden Typ schreiben. Mit Rust oder C++ kann die Notwendigkeit der Codeduplizierung mit Generics und Templates umgangen werden.

  • Numba ist eine stark eingeschränkte Teilmenge von Python, daher kann das Debuggen aufgrund seiner Einschränkungen schwierig sein und kompilierte Sprachen sind flexibler; Cython unterstützt fast alles von Python, Rust ist eine komplexe Sprache mit großartigen Tools usw.

PyPy beschleunigt häufig nicht vektorisierte Operationen ohne Codeänderungen; Sie können Ihren Code einfach ausprobieren und seine Geschwindigkeit sehen. Ansonsten bleibt es, den bestehenden Code zu optimieren, bzw Auswahl geeignete kompilierte Sprache.

In der Zwischenzeit beschleunigt sich Python, wir helfen Ihnen, Ihre Fähigkeiten zu verbessern oder einen jederzeit relevanten Beruf von Anfang an zu meistern:

Wähle ein anderes gefragter Beruf.

Similar Posts

Leave a Reply

Your email address will not be published.