Python ist nicht unbedingt wegen seiner Performanz populär, sondern weil es eine bequeme und entwicklerfreundliche Programmiersprache ist. Doch auch bei Python müssen Sie nicht unbedingt zwischen Ausführungs- und Entwicklungsgeschwindigkeit entscheiden. Richtig optimiert, sind Python-Applikationen überraschend performant. Auch wenn sie nicht die Geschwindigkeitsvorteile von Java oder C realisieren - für Webanwendungen, Data Analytics sowie Management- und Automatisierungs-Tools reicht es. Genauso wie für die meisten anderen Zwecke.
Die Python-Performance zu optimieren, ist allerdings nicht von einem einzelnen Faktor abhängig: Vielmehr kommt es darauf an, alle verfügbaren Best Practices anzuwenden - respektive diejenigen auszuwählen, die für das jeweilige Szenario am besten geeignet sind. Die Entwicklungsabteilung von Dropbox liefert eines der eindrucksvollsten Beispiele dafür, was mit Python-Optimierungen möglich ist.
10 Wege zu mehr Python-Speed
Dieser Artikel stellt zehn gängige Optimierungsmöglichkeiten für Python-Applikationen vor. Dabei handelt es sich um einen Mix aus eher simplen Sofortmaßnahmen und komplexeren Detailarbeiten.
1. Messen, messen, messen
"Was man nicht misst, kann man nicht steuern", wusste schon Management-Guru Peter Drucker. Entsprechend werden Sie wohl nur schwer herauszufinden, warum eine bestimmte Python-App suboptimal läuft, ohne der Sache auf den Grund zu gehen.
Ein optimaler Startpunkt ist deswegen ein einfaches Profiling ihres Codes mit Hilfe des in Python integrierten cProfile
-Moduls. Wenn Sie mehr Präzision oder tiefere Einblicke benötigen, sollten Sie zu einem leistungsfähigeren Profiler wechseln (dazu später mehr). Die Erkenntnisse, die eine grundlegende Funktionsprüfung einer Anwendung zu Tage fördert, sind oft mehr als ausreichend. Über das profilehooks
-Modul können Sie Profildaten für einzelne Funktionen abrufen.
Die Frage, warum ein bestimmter Teil der Anwendung besonders langsam ist und wie man das Problem beheben kann, erfordert unter Umständen eine detailliertere Analyse. Dabei geht es darum, den Fokus einzugrenzen, eine Grundlinie mit konkreten Metriken festzulegen und wann immer möglich eine Vielzahl von Nutzungs- und Deployment-Szenarien zu testen. Aber optimieren sie nicht voreilig: Vermutungen bringen Sie nicht weiter.
Das oben erwähnte Beispiel von Dropbox zeigt, wie nützlich dieses Profiling ist. "Erst die Messungen gaben Aufschluss darüber, dass das HTML Escaping von Anfang an zu langsam war. Und ohne die Performance zu messen, wären wir nie darauf gekommen, dass die String Interpolation so langsam ist wie sie war", heißt es im Blogbeitrag.
2. Wiederkehrende Daten cachen
Warum sollten Sie Tasks tausendfach erledigen, wenn diese auch einmal erledigt und die Ergebnisse abgespeichert werden können? Eine häufig aufgerufene Funktion, die vorhersehbare Ergebnisse liefert, kann in Python zwischengespeichert werden (im Python-Jargon nennt man diesen Vorgang auch "Memoization"). Nachfolgende Calls, die das gleiche Ergebnis liefern, werden nahezu sofort zurückgegeben.
Es gibt diverse Beispiele für die Funktionsweise - einer unser Memoization-Favoriten ist dieses Minimalbeispiel. Allerdings ist diese Funktionalität in Python bereits standardmäßig integriert: Die native Python-Bibliothek functools
verfügt über den @functools.lru_cache
-Dekorator, der die n letzten Aufrufe einer Funktion zwischenspeichert. Dies ist praktisch, wenn sich der Wert, den Sie zwischenspeichern, ändert, aber innerhalb eines bestimmten Zeitfensters relativ statisch bleibt. Eine Liste der im Laufe des Tages zuletzt verwendeten Items wäre ein gutes Beispiel.
Gut zu wissen ist in diesem Zusammenhang: Verwenden Sie das performantere @functools.cache
, wenn Sie sicher sein können, dass sich die Anzahl der Function Calls innerhalb eines vernünftigen Rahmens bewegt (beispielsweise 100 verschiedene zwischengespeicherte Resultate).
3. Mathematik auf NumPy verlagern…
Wenn Sie mit Matrix- oder Array-basierter Mathematik zu tun haben und keinen Wert auf den Python-Interpreter legen, nutzen Sie NumPy. Damit können Sie Arrays schneller verarbeiten - zudem werden numerische Daten effizienter gespeichert als über die in Python integrierten Datenstrukturen. Ein weiterer Benefit von NumPy: Bei größeren Objekten wird der Speicher effizienter genutzt. Etwa, wenn es um Listen mit Millionen von Einträgen geht. Solche Objekte bringen es in NumPy auf etwa ein Viertel des Platzbedarfs - im Vergleich zu konventionellem Python. Zu beachten ist dabei, mit der richtigen Datenstruktur an die Sache heranzugehen. Das stellt für sich bereits eine Optimierung dar.
Python-Algorithmen für die Verwendung von NumPy umzuschreiben, ist allerdings mit Aufwand verbunden, weil Array-Objekte mit der NumPy-Syntax deklariert werden müssen. Zudem lassen sich die größten Geschwindigkeitssteigerungen durch die NumPy-spezifischen Broadcasting-Techniken erzielen, bei denen eine Funktion oder ein Verhalten auf ein Array angewendet wird. Nehmen Sie sich die Zeit und beschäftigen Sie sich ausgiebig mit der NumPy-Dokumentation.
Das Wichtigste zum Schluss: NumPy eignet sich nicht, um mathematische Berechnungen zu beschleunigen, die außerhalb von NumPy-Arrays oder -Matrizen durchgeführt werden. Bei mathematischen Operationen, die konventionelle Python-Objekte einbeziehen, ist folglich kein Geschwindigkeitszuwachs drin.
4. …oder auf Numba
Eine weitere leistungsstarke Bibliothek für schnellere mathematische Berechnungen ist Numba. Schreiben Sie Python-Code für numerische Manipulationen und verpacken Sie ihn mit dem Just-in-Time-Compiler von Numba - der resultierende Code wird mit maschinen-nativer Geschwindigkeit ausgeführt. Dabei bietet Numba nicht nur GPU-getriebene Beschleunigung (sowohl CUDA als auch ROC), sondern verfügt auch über einen speziellen "nopython
"-Modus. Dieser versucht, die Performance zu maximieren, indem er wann immer möglich auf den Python-Interpreter verzichtet.
Numba arbeitet darüber hinaus auch Hand in Hand mit NumPy. Das bedeutet für Sie, das Beste aus zwei Welten miteinander kombinieren zu können: NumPy für alle Operationen, die es lösen kann (siehe oben) - Numba für den Rest.
5. C-Bibliotheken nutzen
In C geschriebene Bibliotheken zu verwenden, ist generell eine gute Strategie. Falls eine C-Bibliothek existiert, die genau das tut, was Sie brauchen, bietet das Python-Ökosystem mehrere Möglichkeiten, um diese (und ihre Geschwindigkeitsvorteile) zu nutzen.
Die gebräuchlichste Möglichkeit, das zu bewerkstelligen, bietet die ctypes
-Library von Python. Weil sie weitgehend mit anderen Python-Anwendungen (und -Laufzeiten) kompatibel ist, bildet sie den optimalen Ausgangspunkt - ist aber bei weitem nicht die einzige Lösung. So bietet beispielsweise das Projekt CFFI eine elegantere Schnittstelle zu C. Um eigene C-Bibliotheken zu schreiben oder externe, bereits vorhandene Bibliotheken zu ummanteln, können Sie auch Cython (siehe Punkt 6) verwenden.
Die besten Ergebnisse werden Sie erzielen, wenn Sie die Anzahl der Übergänge zwischen C und Python minimieren. Jedes Mal, wenn Daten zwischen den beiden Programmiersprachen übertragen werden, resultiert das in Leistungseinbußen. Wenn Sie also die Wahl haben,
entweder eine C-Bibliothek in einem engen Loop zu callen, oder
eine komplette Datenstruktur in die C-Bibliothek zu integrieren und dort das In-Loop-Processing zu erledigen,
sollten Sie sich für Zweiteres entscheiden. Sie werden deutlich weniger "Domain-Roundtrips" machen.
6. Zu Cython konvertieren
C-Code zu schreiben, heißt für Python-Programmierer unter anderem, die Syntax erlernen und mit C-Toolchains hantieren zu müssen. Außer sie benutzen Cython - dann können die Geschwindigkeitsvorteile von C ganz komfortabel und ohne viel Aufwand genutzt werden. Das Python-Superset ermöglicht es, vorhandenen Python-Code schrittweise nach C zu konvertieren. Dazu wird der C-Code zunächst kompiliert und anschließend um Typ-Annotationen für mehr Speed ergänzt.
Doch auch Cython ist kein Zaubertrank: Ohne Type Annotations läuft der Code "lediglich" 15 bis 50 Prozent schneller. Das ist dem Umstand geschuldet, dass sich die meisten Optimierungen auf dieser Ebene darauf konzentrieren, nämlich den Overhead des Python-Interpreters zu reduzieren. Die größten Vorteile ergeben sich, wenn Ihre Variablen als C-Typen annotiert werden können - zum Beispiel ein 64-Bit-Integer auf Maschinenebene (anstelle des Python-Typs int
). Die daraus resultierenden Geschwindigkeitszuwächse können enorm ausfallen.
CPU-gebundener Code profitiert am ehesten von Cython: Wenn Sie ein Code Profiling durchgeführt und dabei festgestellt haben, dass bestimmte Teile das Gros der CPU-Zeit vereinnahmen, sind die optimal geeignet, um in Cython konvertiert zu werden. E/A-gebundener Code - beispielsweise langfristig laufende Netzwerkoperationen - wird von Cython wenig bis gar nicht profitieren.
Ähnlich wie bei den C-Bibliotheken empfiehlt es sich auch bei Cython, so wenig Umwege wie möglich zu gehen: Sehen Sie davon ab, Loops zu schreiben, die wiederholt eine "cythonisierte" Funktion aufrufen. Implementieren Sie den Loop stattdessen in Cython und übergeben Sie die Daten "am Stück".
7. Multiprocessing nutzen
Traditionelle Python-Apps, die in CPython implementiert sind, führen jeweils nur einen einzigen Thread aus, um zu verhindern, dass Probleme auftreten. Das ist der berühmt-berüchtigte Global Interpreter Lock (GIL). Es gibt gute Gründe für seine Existenz - was das Konstrukt allerdings nicht weniger unangenehm macht. Obwohl im Laufe der Zeit deutlich effizienter gestaltet, bleibt das Kernproblem bestehen: CPython erlaubt Multiprocessing nicht wirklich.
Abhilfe schafft an dieser Stelle das Multiprocessing-Modul, das mehrere Instanzen des Python-Interpreters auf separaten Kernen ausführt. Der State kann über gemeinsam genutzten Speicher oder Serverprozesse geteilt, Daten zwischen Prozessinstanzen über "Queues" oder "Pipes" übergeben werden. Zwischen den Prozessen müssen Sie den State jedoch immer noch manuell managen. Zudem ist es mit einem nicht unerheblichen Overhead verbunden, mehrere Python-Instanzen zu starten und Objekte zwischen ihnen zu übergeben. Die Multiprocessing-Bibliothek ist jedoch insbesondere dann nützlich, wenn es um langfristig laufende Prozesse geht, die von der Parallelität über Kerne hinweg profitieren.
Eine Bemerkung am Rande: Python-Module und -Packages, die C-Bibliotheken nutzen (wie NumPy oder Cython), können den GIL vollständig umgehen.
8. Wissen, was Bibliotheken tun…
Natürlich ist es höchst komfortabel, über include foobar
im Handumdrehen auf die Errungenschaften unzähliger anderer Programmierer zuzugreifen. Dabei sollten Sie sich allerdings darüber bewusst sein, dass Drittanbieter-Bibliotheken sich auf die Performance Ihrer Anwendung auswirken können - und zwar nicht unbedingt positiv.
Das ist manchmal relativ offensichtlich (Profiling hilft auch an dieser Stelle) - manchmal eher nicht. Ein Beispiel: die Bibliothek Pyglet. Sie aktiviert automatisch einen Debug-Modus, der die Performance dramatisch drückt - solange, bis das explizit abgestellt wird. Wenn Sie darüber allerdings nicht informiert sind, fällt es Ihnen möglicherweise gar nicht erst auf.
9. …und die Plattform
Python funktioniert zwar plattformübergreifend, das bedeutet allerdings nicht, dass die Eigenheiten der einzelnen Betriebssysteme einfach verschwinden. In den allermeisten Fällen lohnt es sich, plattformspezifische Besonderheiten wie Namenskonventionen auf dem Schirm zu haben, für die Hilfsfunktionen zur Verfügung stehen. Das Modul pathlib abstrahiert beispielsweise solche plattformspezifischen Pfadkonventionen. Auch die Konsole wird unter den jeweiligen Betriebssystemen sehr unterschiedlich gehändelt - daher erfreuen sich abstrahierende Bibliotheken steigender Beliebtheit. Ein weiteres Beipsiel in diesem Bereich ist rich.
Bestimmte Funktionen werden auf manchen Plattformen überhaupt nicht unterstützt. Das kann sich auf die Art und Weise auswirken, wie Sie Python-Code schreiben: Windows kennt zum Beispiel das Process-Forking-Konzept nicht, weswegen einige Multiprocessing-Funktionen anders funktionieren.
Schließlich spielt auch die Art und Weise eine Rolle, wie Python selbst auf der jeweiligen Plattform installiert und ausgeführt wird. Unter Linux wird pip
beispielsweise in der Regel getrennt von Python installiert - im Gegensatz zu Windows.
10. PyPy verwenden
Bei CPython hat Kompatibilität Vorrang vor Geschwindigkeit. Speed-affine Programmierer setzen deshalb oft auf PyPy, ein Drop-In-Ersatz für CPython. Dieser ist mit einem Just-in-Time-Compiler ausgestattet, was die Ausführung von Code beschleunigt, und bietet eine der simpelsten Möglichkeiten, um schnell Leistungsschübe zu erzielen. Die Mehrheit der gängigen Python-Apps muss für PyPy nicht angepasst werden.
Trotzdem bleiben Testing- und Experimentier-Sessions nicht aus, wenn Sie die Benefits von PyPy optimal für sich nutzen wollen. Sie werden dabei feststellen, dass langfristig laufende Applikationen am meisten von PyPy profitieren, weil dessen Compiler die Execution im Zeitverlauf analysiert, um zu ermitteln, wie sich die Performanz steigern lässt. Für kurze Skripte, die nur ausgeführt und dann beendet werden, eignet sich CPython meist besser, weil die Performance-Zugewinne den JIT-Overhead nicht ausgleichen können.
Behalten Sie dabei im Hinterkopf, dass PyPy der jeweils aktuellsten Python-Version "hinterherhinkt": Zu dem Zeitpunkt, als Python 3.12 aktuell war, bot PyPy beispielsweise lediglich Support bis Version 3.10. Außerdem wichtig: Python-Anwendungen, die ctypes
nutzen, verhalten sich möglicherweise nicht immer wie erwartet. Wenn Sie etwa schreiben, das sowohl auf PyPy als auch auf CPython laufen könnte, ist es unter Umständen sinnvoll, für jeden Interpreter einen separaten Use Case zu fahren. (fm)