Python: Schneller als man denkt

  Götz   Lesezeit: 5 Minuten  🗪 4 Kommentare Auf Mastodon ansehen

Beginn einer Artikelserie zum Thema schneller Python Code. Eine Einführung mit einer Auflistung verschiedener Strategien die verglichen werden.

python: schneller als man denkt

Wir alle kennen die Programmiersprache Python. Sie ist durch ihre Einfachheit, weite Verbreitung und Fähigkeit zur Interoperabilität mit verschiedensten Programmen und Programmiersprachen DAS „Schweizer Taschenmesser“.

Diese Flexibilität wird durch Geschwindigkeit bezahlt, die Sprache ist aufgrund ihrer „dynamischen Typisierung“ erst zur Laufzeit fähig die Datentypen der Methoden und Variablen aufzulösen. Das Konzept wird genauer als „Duck-Typing“ im Python Umfeld bezeichnet. Die Struktur eines Datums (Objekt) muss einen Methodensatz zur Verfügung stellen, der die gewünschte Operation ermöglicht. Gleich dem Motto „Wenn es wie eine Ente läuft, wie eine Ente schwimmt und wie eine Ente quakt, dann ist es wahrscheinlich eine Ente“.

Doch was genau heißt das? In Python sind wir in der Lage so Äpfel mit Birnen zu vergleichen, solange das linke Objekt des Vergleichsoperators (linke Objekt == rechte Objekt) die Methode

class Apfel():
    def __eq__(self, obj):
        if obj.value == self.value:
            return True
        else:
            return False


definiert hat und dieses Objekt mit Birnen vergleichen kann.

Innerhalb dieser Vergleichsoperation werden Felder oder Methoden angefragt und ausgewertet, gibt es diese nicht, weicht der Methodenkopf in Anzahl der Parameter ab, bzw. hat überhaupt keinen Rückgabewert ist Python das nur in der Lage dynamisch auszuwerten. Selbst wenn eine Methode keinen Rückgabewert hat, ist das einfach None.

Der Python Interpreter muss damit jede Methode durchlaufen und kann erst am Ende der Ausführung feststellen, ob der zurückgegebene Wert auch das kann, was von ihm erwartet wird. Dies setzt eine ständige Prüfung voraus, die ständig wertvolle Ressourcen verbraucht und die Sprache extrem verlangsamt. [1]

In Python gibt es zur Lösung dieses Problems verschiedene Strategien:

  1. Nutze schneller Konstrukte, die weniger Abstraktionen durch die Standardimplementierung erfahren
  2. Nutze Bibliotheken, die diese Funktionalität in schnellere Sprachen ausgelagert haben
  3. Schreibe deinen Code einfach nicht in Python

All diese Strategien mögen theoretisch angewendet werden, doch sind sie weitestgehend quatsch. Von einem Programmierer zu erwarten, das er die „schnellen Sprachkonstrukte“ von den „langsamen Sprachkonstrukten“ unterscheiden kann ist ein Designfehler der Sprache, der versucht die Verantwortung an den Nutzenden abzuladen. Wer ein Werkzeug zur Verfügung stellt, mit dem Hinweis, dass seine Nutzung eine dumme Idee ist, sollte es einfach lassen.

Ebenso stellt man fest, dass der geschriebene Code zu langsam ist und muss sich dann auch noch nach einer Bibliothek umsehen, die die gleiche oder ähnliche Funktionalität bereitstellt. Man kennt den Code nicht, den Maintainer und kann die Zuverlässigkeit nicht einschätzen.

Gibt es keine schnelleren Konstrukte und keine Bibliothek schreibt man seinen Code schlussendlich in einer Sprache, in der man keine Übung und wenig Ahnung hat. Funktioniert es dann endlich, so bleibt von Python nur das Abstraktionslayer und die „Ich-teste-mal-meine-Idee“ Funktion über. Auf einen zu kommt der unbekannter Aufwand, den Python Code in eine schnellere Sprache zu überführen.

Wie lösen wir das Problem? Durch Teilen und Herrschen:

Domänenspezifische Bibliotheken

Wir verwenden in Problemstellungen spezifische Bibliotheken, die Funktionalitäten von Python ersetzen. Unser folgendes Beispiel wird aus der Mathematik kommen, daher ist hier die Wahl die Bibliothek NumPy. Sie ermöglicht mathematische Operationen auf ganze Datenfelder anzuwenden. Zusätzlich werden Schaltkreise der CPU und auch GPU verwendet, die nicht durch die Optimierung des Compilers zur Verfügung stehen. Prozeduren sind in C geschrieben, um möglichst hardwarenah und damit schnell zu rechnen. [2]

JIT Bibliotheken

Manchmal bieten domänenspezifische Bibliotheken nicht die entsprechenden Funktionen und Python Code lässt sich nicht weiter optimieren. In solchen Fällen ist es sinnvoll einen Just-In-Time (JIT) Compiler (Numba) zu verwenden, welcher die Prozeduren in Python dynamisch zur Laufzeit in C-Code umwandelt und zu Maschinencode compiliert. Dieser Ansatz benötigt eine vollständige Toolchain und verzögert die Ausführung des Codes bei Erstverwendung. Auch wenn die Entwicklergemeinschaft dieser Projekte es immer so darstellen, als wäre es ein Kinderspiel diese Systeme zu verwenden, so sind Einschränkungen gegenüber dem vollen Funktionsumfang von Python gegeben. Zusätzlich hat die Entwicklerin keinen Einfluss auf das Transpiling des Codes. Nur durch Erfahrung, Tests und ein profundes Wissen ist es möglich diese Tools zu jeder Zeit sinnvoll zu verwenden, da sie selbst als eine Black-Box arbeiten. [3]

Transpiler

Werden ganze Algorithmen entwickelt, so ist es später nicht sinnvoll das Code aus komplexen Bibliothekskonstellationen besteht. Er ist schwer zu warten und erhöht die Abhängigkeit gegenüber Dritten enorm. Es bietet sich daher an den Code in einer hardwarenahen Sprache neu zu schreiben. Das reduziert die Komplexität des Projektes, macht es für die einzelne Entwicklerin jedoch kompliziert: Konstrukte die in Python funktionieren sind schwer in anderen Sprachen zu reproduzieren. Sie benötigen ihrerseits Bibliotheken und viel Pflege. Wäre es da nicht sinnvoll fließend zwischen Python und einer „schnellen, aber komplizierten Version“ fließend zu wechseln?

Das Projekt Cython ermöglicht dies durch einen Transpiler, der die Syntax von Python durch C/C++ Konstrukte - sowie einer statischen Typisierung ergänzt. Der generierte C-Code enthält den originalen Python Code als Kommentar und kann von der Entwicklerin nach Fehlern durchsucht bzw. durch eine statische Typprüfung auf Fehler abgeklopft werden. Besonders angenehm macht die Benutzung, dass Cython es fließend zulässt zwischen „reinem“ Python Code und C-Code zu wechseln.

In der kommenden Serie werde ich verschiedene Ansätze durchgehen und die Ergebnisse präsentieren. Dabei wird die Problemstellung und die verwendeten Ansätze präsentiert. Der Code wird auf Codeberg zur Verfügung gestellt. [4]

[Hauptbild] ChatGPT 5: "Generate me a logo made up of the python logo which is merging with HPC or C-language logo to symbolize fast computing"

[1] https://peps.python.org/pep-0544

[2] https://numpy.org/doc/stable/user/whatisnumpy.html

[3] https://numba.readthedocs.io/en/stable/user/5minguide.html#how-does-numba-work

[4] https://cython.readthedocs.io/en/latest/src/quickstart/overview.html

Tags

Python, Cython, Numpy, Numba

Lenny
Geschrieben von Lenny am 12. August 2025 um 10:05

Ist nicht haupsächlich die Qualität des eingesetzten Compiler für die Geschwindigkeit eines Programms verantwortlich? Spezialbibliotheken führen nicht automatisch zu schnelleren Programmen und Laufzeit-Interpreter sind eine ganz andere Kategorie. Am Ende übersetzen alle Compiler in für die jeweilige Plattform gültigen Maschinencode. Bei C und CPP gibt es historisch bedingt sehr gute Compiler und deswegen bietet sich der Einsatz eines C-Transpilers an. Unabhängig von der eingesetzten Sprache sind es die Verbesserungen am Compiler, die besseren Binärcode erzeugen und damit Anwendungen beschleunigen.

xnor
Geschrieben von xnor am 28. August 2025 um 12:55

Nicht wenn die Sprache solche Designentscheidungen getroffen hat, dass man gar keinen Compiler schreiben KANN, der Code generiert, der auch nur im Entferntesten mit "optimalem" C o. ä. vergleichbar wäre. C Compiler sind nur so gut, weil die Sprache an sich schon low level ist und zusätzlich vieles offen lässt (siehe auch undefined behavior).

xnor
Geschrieben von xnor am 28. August 2025 um 12:47

Python ist nicht für performante Software gemacht, sondern für Scripting und Prototyping. Nur weil man eine Banane in die Form eines Apfels drücken kann, erhält man deswegen noch lange keinen Apfel.

Götz
Geschrieben von Götz am 31. August 2025 um 02:39

Ich schreibe schnellen Python-Code, indem ich Performance kritischen Code eben NICHT in Python, sondern in C/C++ schreibe. Python Code ist sehr gut einzusetzen als Interface, indem verschiedene Aspekte miteinander verbunden werden.

Korrekt ist, dass Python AUCH eine sehr gut Scripting und Prototyping Sprache ist.