
DIgilife - stock.adobe.com
Code-Optimierung in Python: Von Profiling bis Cython
Mit den richtigen Optimierungstechniken ist ein Geschwindigkeitsgewinn in Python kein Problem. Profiling, Cython und JIT-Compiler machen den Unterschied.
Python kann im Vergleich zu kompilierten Sprachen wie C oder Rust langsam sein. Durch gezielte Optimierungstechniken lässt sich aber eine deutliche Steigerung der Laufzeit erreichen. Statt wahllos Code zu optimieren, lohnt sich zuerst eine Analyse der Engpässe.
cProfile, line_profiler, memory_profiler und Py-Spy nutzen
Ein gutes Profiling-Tool wie cProfile kann aufzeigen, welche Funktionsaufrufe den größten Teil der Rechenzeit beanspruchen. Es unterscheidet zwischen primitiven und rekursiven Aufrufen und ermöglicht eine Sortierung der Ergebnisse nach verschiedenen Kriterien wie Gesamtzeit oder Häufigkeit der Funktionsaufrufe. Die Analyse kann direkt in der Kommandozeile erfolgen oder innerhalb des Codes mit pstats weiterverarbeitet werden. Ergebnisse lassen sich als Profildatei speichern und mit Tools wie tuna visuell darstellen.
Noch granularer arbeitet line_profiler, der den Zeitaufwand pro Zeile misst. Um Speicherengpässe zu erkennen, hilft wiederum memory_profiler. Py-Spy arbeitet wiederum ohne Overhead und eignet sich besonders für Live-Analysen. Eine typische Methode zur Laufzeitanalyse besteht darin, einzelne Funktionsaufrufe mit time.time() oder perf_counter() zu messen. Diese Methode kann jedoch mühsam sein, weshalb cProfile eine automatisierte Analyse ermöglicht. Um cProfile direkt in der Kommandozeile zu nutzen, kann das folgende Kommando verwendet werden:
python -m cProfile main.py
Alternativ kann das Profiling direkt im Code erfolgen:
import cProfile
import pstats
def test_function():
total = 0
for i in range(1000000):
total += i
return total
with cProfile.Profile() as pr:
test_function()
stats = pstats.Stats(pr)
stats.sort_stats(pstats.SortKey.TIME)
stats.print_stats()
Noch anschaulicher wird die Analyse mit tuna, einem Tool zur Visualisierung von Profilergebnissen. Dieses kann mit folgendem Befehl installiert werden:
pip install tuna
Nach dem Profiling kann eine visuelle Analyse mit folgendem Kommando gestartet werden:
tuna results.prof
Die Standardbibliothek für bessere Performance verwenden
Viele Python-Entwickler unterschätzen die Geschwindigkeit der Standardbibliothek. Built-in-Funktionen wie sorted() oder sum() sind in C implementiert und laufen erheblich schneller als eigens geschriebene Alternativen. Ein selbstgebauter Sortieralgorithmus kommt kaum an die Geschwindigkeit der eingebauten Funktion heran, da Python unter der Haube eine optimierte Implementierung verwendet.
Ein direkter Vergleich zeigt den Unterschied:
import time
import random
def custom_sort(arr):
return sorted(arr)
arr = [random.randint(0, 10000) for _ in range(100000)]
start = time.time()
custom_sort(arr)
print("Custom sort:", time.time() - start)
start = time.time()
sorted(arr)
print("Built-in sort:", time.time() - start)
Die Wahl der richtigen Datenstruktur hat ebenfalls einen Einfluss. Wer häufig nach Elementen sucht, sollte lieber ein Set oder ein Dictionary nutzen statt einer Liste, da Lookups in diesen Strukturen erheblich schneller sind.
Effiziente Speicherverwaltung mit Generatoren
Eine weitere Optimierungsmöglichkeit ist der gezielte Einsatz von Generatoren. Statt eine komplette Liste im Speicher zu halten, erzeugt ein Generator die Werte nur bei Bedarf. Das spart Speicher und reduziert unnötige Kopiervorgänge. Eine Funktion mit yield verhält sich wie eine pausierbare Schleife. Dies ist besonders vorteilhaft, wenn mit großen Datenmengen gearbeitet wird, beispielsweise beim zeilenweisen Einlesen großer Dateien:
def read_large_file(file_path):
with open(file_path, 'r') as file:
for line in file:
yield line.strip()
Parallelisierung und das GIL-Problem
Python-Programme laufen standardmäßig auf einem einzelnen CPU-Kern, da das Global Interpreter Lock (GIL) echte Multithreading-Parallelisierung verhindert. Für CPU-intensive Aufgaben kann das multiprocessing-Modul jedoch mehrere Prozesse erzeugen, die unabhängig voneinander arbeiten.
Python 3.13 nutzt eine experimentelle Option zur Deaktivierung des GIL. Ohne GIL können Threads effektiver arbeiten, insbesondere bei parallelen Berechnungen. Allerdings kann das Entfernen des GIL in manchen Fällen zu einer Verschlechterung der Single-Thread-Performance führen. Zudem müssen bestehende Bibliotheken wie FastAPI, SQLAlchemy oder Django möglicherweise angepasst werden, da sie auf das GIL angewiesen sind.
Kompilierte Python-Alternativen für maximale Geschwindigkeit
Neben der Optimierung einzelner Codebestandteile gibt es auch Just-in-Time und Ahead-of-Time Compiler für Python. mypyc kompiliert Python-Code mit statischen Typen direkt zu C-Erweiterungen und kann eine neunfache Beschleunigung gegenüber reinem Python erreichen. Die Nutzung ist einfach:
mypyc my_script.py
Eine weitere Möglichkeit bietet Cython, das eine Mischung aus Python und C++-Syntax erlaubt. Noch größere Performance-Sprünge lassen sich mit cython.locals und cython.view erzielen, da so die Rückkehr in den Python-Interpreter minimiert wird.
cython
cdef int add(int a, int b):
return a + b
Numba ist eine noch einfachere Lösung für numerische Berechnungen und kann mit einem einzigen Dekorator große Geschwindigkeitssprünge erzielen:
from numba import jit
@jit(nopython=True)
def sum_array(arr):
total = 0
for i in arr:
total += i
return total
Im Benchmark zum Longest Common Subsequence-Problem zeigt sich, dass Numba C++-ähnliche Geschwindigkeiten erreichen kann, oft sogar schneller als eine manuell geschriebene C-Implementierung.
Taichi für spezialisierte Hochleistungsberechnungen
Für performante Berechnungen, insbesondere in Simulationen und Grafikverarbeitung, bietet sich Taichi Lang an. Diese domänenspezifische Sprache basiert auf Python und erlaubt es, Code für verschiedene Hardware-Architekturen zu optimieren. Besonders beeindruckend ist, dass ein statisch allokiertes Array in Taichi Lang eine noch schnellere Performance als C++ erreichen kann:
import taichi as ti
ti.init(arch=ti.cpu)
@ti.kernel
def compute():
for i in range(10000):
pass
PyPy als Alternative zum CPython-Interpreter
Neben der Optimierung einzelner Codebestandteile gibt es Alternativen zum Python-Interpreter selbst. PyPy nutzt Just-in-Time-Kompilierung und kann viele Programme erheblich beschleunigen. Besonders für langlaufende Prozesse, die immer wieder dieselben Funktionen aufrufen, bietet PyPy eine enorme Performance-Steigerung.
pypy my_script.py
MyPyC, Cython und Numba erfordern eine Optimierung des Codes selbst, PyPy kann oft als direkter Ersatz für den CPython-Interpreter genutzt werden. Allerdings unterstützt nicht jede Bibliothek diesen Interpreter, weshalb Tests vor dem Wechsel notwendig sind.