Was ist Cython?

Mit Cython machen Sie Ihrem Python-Code Beine.


Foto: Avigator Fortuner – shutterstock.com

Python steht im Ruf, eine der komfortabelsten, am besten ausgestatteten und nützlichsten Programmiersprachen zu sein. Ausführungsgeschwindigkeit zählt allerdings nicht zu ihren Stärken. An dieser Stelle kommt Cython ins Spiel: Die Sprache ist ein Superset von Python und wird in C kompiliert. Das führt zu teils drastischen Leistungssteigerungen:

  • Im Fall von Aufgaben, die an die aktiven Objekttypen von Python gebunden sind, sind die Performance-Steigerungen überschaubar.
  • Bei numerischen und allen anderen Operationen, die nicht mit Python-Interna in Zusammenhang stehen, können die Gewinne enorm sein.

Mit Hilfe von Cython können Sie viele der nativen Beschränkungen von Python umgehen oder sie ganz überwinden – ohne dabei auf dessen Einfachheit und Komfort verzichten zu müssen. Dieser Artikel erläutert die grundlegenden Konzepte von Cython und demonstriert zudem, wie Sie es zum Einsatz bringen.

Python-Code kann direkt in C-Module kompiliert werden. Bei diesen kann es sich entweder um allgemeine C-Bibliotheken oder um solche handeln, die speziell für die Zusammenarbeit mit Python entwickelt wurden. Cython erzeugt die zweite Art von Modulen: C-Bibliotheken, die mit den Interna von Python kommunizieren und mit bestehendem Python-Code gebündelt werden können.

Cython-Code ähnelt seinem Python-Pendant von Haus aus sehr stark. Wenn Sie den Cython-Compiler mit einem Python-Programm füttern (Python 2.x und Python 3.x werden unterstützt), wird Cython es so akzeptieren, wie es ist. Allerdings werden keine nativen Beschleunigungsfunktionen ins Spiel kommen. Wenn Sie jedoch den Python-Code mit Typ-Annotationen in spezieller Cython-Syntax ausschmücken, kann Cython die langsamen Python-Objekte durch schnelle C-Äquivalente ersetzen.

Dabei sollten Sie beachten, dass der Ansatz von Cython inkrementell ist: Entwickler starten mit einer bestehenden Python-Anwendung und beschleunigen diese durch punktuelle Änderungen am Code – statt sie komplett neu zu schreiben. Dieser Ansatz deckt sich mit der Natur von Software-Performance-Problemen im Allgemeinen. In den meisten Programmen ist der Großteil des rechenintensiven Codes auf einige wenige Brennpunkte konzentriert – eine Version des Paretoprinzips, auch bekannt als “80/20”-Regel. Daher muss auch der größte Teil des Codes einer Python-Anwendung nicht leistungsoptimiert werden, sondern nur einige wenige, kritische Teile. Diese können Sie schrittweise in Cython übersetzen, um die Leistungssteigerung genau dort zu erzielen, wo sie am wichtigsten ist. Der Rest des Programms kann in Python bleiben, ohne dass zusätzliche Arbeit anfällt.

Folgendes Code-Beispiel entstammt der Cython-Dokumentation:

def f(x):

return x**2-x

def integrate_f(a, b, N):

s = 0

dx = (b-a)/N

for i in range(N):

s += f(a+i*dx)

return s * dx

Hierbei handelt es sich nur um ein Experimentierbeispiel – die nicht besonders effiziente Implementierung einer Integralfunktion. Als reiner Python-Code ist sie langsam, weil Python zwischen maschinennativen numerischen Typen und seinen eigenen internen Objekttypen hin und her konvertieren muss. Betrachten wir nun die Cython-Version desselben Codes:

cdef double f(double x):

return x**2-x

def integrate_f(double a, double b, int N):

cdef int i

cdef double s, dx

s = 0

dx = (b-a)/N

for i in range(N):

s += f(a+i*dx)

return s * dx

Wenn wir die Variablentypen sowohl für die Funktionsparameter als auch für die im Körper der Funktion verwendeten Variablen (double, int, etc.) explizit angeben, übersetzt Cython all dies in C. Um schneller zu sein, können Sie auch das Keyword cdef verwenden, um Funktionen zu definieren, die hauptsächlich in C implementiert sind. Im obigen Beispiel kann nur integrate_f von einem anderen Python-Skript aufgerufen werden, da es def verwendet. Auf cdef-Funktionen kann von Python aus nicht zugegriffen werden, da sie reine C-Funktionen sind und keine Python-Schnittstelle haben.

Beachten Sie, wie wenig sich der eigentliche Code geändert hat. Wir haben lediglich Typendeklarationen in den bestehenden Code eingefügt, um eine deutliche Leistungssteigerung zu erzielen.

Cython bietet zwei Möglichkeiten, seinen Code zu schreiben. Das obige Beispiel verwendet die ursprüngliche Cython-Syntax, die vor der Einführung der modernen Python-Syntax mit Typ-Hinweisen entwickelt wurde. Mit der neueren Cython-Syntax, dem Pure-Python-Modus, können Sie jedoch Code schreiben, der näher an der Python-Syntax liegt, einschließlich der Typendeklarationen.

Der obige Code würde im Pure-Python-Modus etwa so aussehen:

import cython

@cython.cfunc

def f(x: cython.double) -> cython.double:

return x**2 - x

def integrate_f(a: cython.double, b: cython.double, N: cython.int):

s: cython.double = 0

dx: cython.double = (b - a) / N

i: cython.int

for i in range(N):

s += f(a + i * dx)

return s * dx

Im Pure-Python-Modus ist Cython etwas einfacher zu verstehen und kann auch von nativen Python-Linting-Tools verarbeitet werden. Außerdem können Sie den Code so ausführen, wie er ist, ohne zu kompilieren (allerdings ohne die Geschwindigkeitsvorteile). Es ist sogar möglich, den Code abhängig davon auszuführen, ob er kompiliert wurde oder nicht. Leider sind einige der Funktionen von Cython, wie die Arbeit mit externen C-Bibliotheken, im Pure-Python-Modus nicht verfügbar.

Abgesehen von der Möglichkeit, bereits geschriebenen Code zu beschleunigen, bietet Cython weitere Vorteile:

Bessere Performance mit externen C-Bibliotheken

Python-Pakete wie NumPy verpacken C-Bibliotheken in Python-Interfaces, um diese leichter verarbeiten zu können. Über diese Wrapper zwischen Python und C hin- und herzuwechseln, kann jedoch die Arbeit verlangsamen. Mit Cython können Sie direkt mit den zugrundeliegenden Bibliotheken kommunizieren (C++-Bibliotheken werden ebenfalls unterstützt).

Memory Management von C und Python verwenden

Wenn Sie Python-Objekte verwenden, läuft Memory Management und Garbage Collection wie bei regulärem Python ab. Sie können auch Ihre eigenen C-Strukturen erstellen und managen und über den Befehl malloc/free mit diesen arbeiten. Dabei sollten Sie daran denken, hinterher aufzuräumen.

Sicherheit oder Geschwindigkeit

Cython überprüft die Laufzeit automatisch auf gängige C-Probleme (etwa ein Out-of-Bounds-Zugriff auf ein Array) – und zwar mit Hilfe von Dekoratoren und Compiler-Direktiven (zum Beispiel @boundcheck(False)). In der Konsequenz ist der von Cython generierte C-Code standardmäßig wesentlich sicherer als ein handgeschriebenes Pendant – wenn auch möglicherweise auf Kosten der Performance. Wenn Sie sicher sind, diese Prüfungen zur Laufzeit nicht zu benötigen, können Sie diese deaktivieren, um zusätzliche Geschwindigkeitsgewinne zu erzielen – entweder für ein ganzes Modul oder nur für ausgewählte Funktionen.

Mit Cython können Sie auch nativ auf Python-Strukturen zugreifen, die das Buffer Protocol für den direkten Zugriff auf die im Speicher abgelegten Daten verwenden (ohne Zwischenkopien). Cythons Memory Views ermöglichen es Ihnen, mit diesen Strukturen in hoher Geschwindigkeit und mit dem für die jeweilige Aufgabe geeigneten Sicherheitsniveau zu arbeiten. So können beispielsweise die Rohdaten, die einer Python-Zeichenkette zugrunde liegen, auf diese Weise (schnell) gelesen werden, ohne dass die Python-Laufzeit (langsam) durchlaufen werden muss.

Von GIL-Freigaben profitieren

Pythons Global Interpreter Lock (GIL) synchronisiert Threads innerhalb des Interpreters, schützt den Zugriff auf Python-Objekte und regelt den “Wettbewerb” um Ressourcen. Aber die GIL wurde weithin als Stolperstein für ein leistungsfähigeres Python kritisiert, insbesondere auf Multicore-Systemen.

Wenn Sie einen Codeabschnitt haben, der keine Verweise auf Python-Objekte enthält und eine längliche Operation ausführt, können Sie ihn mit der Anweisung with nogil: markieren, damit er ohne die GIL ausgeführt werden kann. Dadurch kann der Python-Interpreter in der Zwischenzeit andere Dinge tun, während Cython-Code (mit zusätzlichem Aufwand) mehrere Kerne nutzen kann.

Sensiblen Code verschleiern

Python-Module sind leicht zu dekompilieren und zu inspizieren, kompilierte Binärdateien hingegen nicht. Wenn Sie eine Python-Anwendung an Endbenutzer verteilen und dabei einige Module vor Schnüffeleien schützen wollen, können Sie das tun, indem Sie sie mit Cython kompilieren.

Dabei sollten Sie jedoch beachten, dass diese Verschleierung lediglich ein Nebeneffekt von Cython ist und keine beabsichtigte Funktion darstellt. Zudem sollten Sie sich klarmachen, dass es nicht unmöglich ist, eine Binärdatei zu dekompilieren oder per Reverse Engineering nachzubilden, wenn man engagiert oder entschlossen genug ist. Außerdem sollten Geheimnisse wie Token oder andere sensible Informationen niemals in Binärdateien versteckt werden – denn diese lassen sich oft mit einem einfachen Hex-Dump enttarnen.

Kompilierte Module weiterverteilen

Wenn Sie ein Python-Paket erstellen, das an andere weitergegeben werden soll, entweder intern oder über PyPI, können darin mit Cython kompilierte Komponenten enthalten sein. Diese Komponenten können für bestimmte Rechnerarchitekturen vorkompiliert werden, auch wenn Sie für jede Architektur separate Python-Wheels erstellen müssen. Andernfalls kann der Benutzer den Cython-Code als Teil des Installationsprozesses kompilieren, sofern ein C-Compiler auf dem Zielrechner verfügbar ist.

Bei allen Vorteilen sollten Sie nicht vergessen, dass Cython kein Zaubermittel ist: Es verwandelt nicht automatisiert jede mickrige Python-Instanz in blitzschnellen C-Code. Wenn Sie das Optimum aus Cython herausholen wollen, müssen Sie es mit Bedacht einsetzen und sollten sich seiner Limitationen bewusst sein.

Minimaler Speed-Zuwachs für konventionellen Python-Code

Wenn Cython Python-Code nicht vollständig in C übersetzen kann, wandelt es diesen in eine Reihe von C-Calls um, die an die Python-Interna gehen. Das führt dazu, dass der Python-Interpreter aus der Ausführungsschleife herausgenommen wird, was dem Code standardmäßig eine bescheidene Beschleunigung von 15 bis 20 Prozent verleiht. Beachten Sie, dass es sich hierbei um ein Best-Case-Szenario handelt: In manchen Situationen kann es sein, dass Sie keine Leistungssteigerung oder sogar eine Verschlechterung feststellen. Um die Auswirkungen zu überprüfen, sollten Sie die Performance vorher und nachher messen.

Geringe Zuwächse bei nativen Python-Datenstrukturen

Python bietet eine ganze Reihe von Datenstrukturen (beispielsweise Strings, Listen, Tuples oder Dictionaries). Die sind für Entwickler äußerst praktisch und verfügen über eine eigenes automatisches Memory Management. Aber sie sind langsamer als reines C.

Mit Cython können Sie weiterhin alle Python-Datenstrukturen verwenden, wenn auch ohne großen Geschwindigkeitszuwachs. Das liegt daran, dass Cython einfach die C-APIs in der Python-Laufzeitumgebung aufruft, mit denen diese Objekte erstellt und bearbeitet werden. Daher verhalten sich Python-Datenstrukturen im Allgemeinen ähnlich wie Cython-optimierter Python-Code: Sie bekommen manchmal einen Schub, aber nur einen kleinen. Die besten Ergebnisse erzielen Sie, wenn Sie C-Variablen und -Strukturen verwenden. Die gute Nachricht: Cython macht es einfach, damit zu arbeiten.

Cython-Code läuft in “Pure C” am schnellsten

Wenn Sie eine Funktion in C mit all ihren Variablen und Inline-Funktionsaufrufen zu anderen Dingen, die reines C sind, mit dem Schlüsselwort cdef gekennzeichnet haben, wird sie so schnell laufen, wie C es erlaubt. Wenn diese Funktion jedoch auf Python-eigenen Code verweist, etwa auf eine Datenstruktur oder einen internen API-Call, führt dieser zu Leistungseinbußen.

Glücklicherweise bietet Cython eine Möglichkeit, diese Engpässe zu erkennen: Ein Source Code Report zeigt auf einen Blick, welche Teile Ihrer Cython-Anwendung reines C sind und welche Teile mit Python interagieren. Je besser die Anwendung optimiert ist, desto weniger Interaktion mit Python wird es geben.

Ein Source Code Report für eine Cython-Applikation. Die weißen Bereiche sind “Pure C” – die gelben Bereiche weisen Interaktionen mit den Python-Interna aus.

Cython optimiert die Nutzung von C-basierten Bibliotheken von Drittanbietern wie NumPy. Da der Cython-Code nach C kompiliert wird, kann er direkt mit diesen Bibliotheken interagieren und die Engpässe von Python eliminieren.

Insbesondere NumPy arbeitet gut mit Cython zusammen: Cython bietet native Unterstützung für bestimmte Konstruktionen in NumPy und schnellen Zugriff auf NumPy-Arrays. Dabei können Sie die gleiche, vertraute NumPy-Syntax in Cython verwenden, die Sie auch in einem herkömmlichen Python-Skript nutzen würden.

Wenn Sie eine möglichst enge Verbindung zwischen Cython und NumPy herstellen wollen, müssen Sie den Code mit der benutzerdefinierten Syntax von Cython weiter ausschmücken. Die cimport-Anweisung ermöglicht es dem Cython-Code zum Beispiel, Konstrukte auf C-Ebene in Bibliotheken zur Kompilierzeit zu sehen, um schnellstmöglich Verbindungen zu schaffen.

Da NumPy so weit verbreitet ist, unterstützt Cython NumPy “out of the box“. Wenn Sie NumPy installiert haben, können Sie einfach cimport numpy anfügen und dann weitere Dekoration hinzufügen, um die exponierten Funktionen zu verwenden.

Um Ihrem Code die maximale Performance zu entlocken, sollten Sie diesen profilieren und ermitteln, wo Bottlenecks bestehen. Cython bietet Hooks für das cProfile-Modul von Python, so dass Sie dazu dieses Python-eigene Profiling-Tool verwenden können.

Je weniger Sie zwischen Python und Cython hin- und herpendeln, desto schneller wird Ihre Anwendung laufen. Wenn Sie zum Beispiel eine Sammlung von Objekten haben, die Sie in Cython verarbeiten wollen, sollten Sie sie nicht in Python durchlaufen und bei jedem Schritt eine Cython-Funktion aufrufen. Übergeben Sie die gesamte Sammlung an Ihr Cython-Modul und führen Sie die Iteration dort durch. Diese Technik wird häufig in Bibliotheken verwendet, die Daten verwalten, so dass es ein gutes Modell ist, das Sie in Ihrem eigenen Code nachahmen können. (fm)

Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.

https://www.computerwoche.de/a/was-ist-cython,3613603

Leave a Reply