Python ist langsam. Warum?

In letzter Zeit kann man die wachsende Popularität der Programmiersprache Python beobachten. Es wird in DevOps, in der Datenanalyse, in der Webentwicklung, im Sicherheitsbereich und in anderen Bereichen verwendet. Aber hier ist die Geschwindigkeit ... Von dieser Sprache gibt es hier nichts zu rühmen. Der Autor des Materials, dessen Übersetzung wir heute veröffentlichen, hat beschlossen, die Gründe für die Langsamkeit von Python herauszufinden und Mittel zu finden, um es zu beschleunigen.



Allgemeine Bestimmungen


In welcher Beziehung steht Java in Bezug auf die Leistung zu C oder C ++? Wie vergleiche ich C # und Python? Die Antworten auf diese Fragen hängen stark von der Art der vom Forscher analysierten Anwendungen ab. Es gibt keinen perfekten Benchmark, aber das Studium der Leistung von Programmen, die in verschiedenen Sprachen geschrieben wurden. Das Computersprachen-Benchmark-Spiel kann ein guter Ausgangspunkt sein .

Ich beziehe mich seit mehr als zehn Jahren auf das Computersprachen-Benchmark-Spiel. Python ist im Vergleich zu anderen Sprachen wie Java, C #, Go, JavaScript, C ++ eine der langsamsten . Dies umfasst Sprachen, die JIT- Kompilierung (C #, Java) und AOT- Kompilierung (C #, C ++) verwenden, sowie interpretierte Sprachen wie JavaScript.

An dieser Stelle möchte ich darauf hinweisen, dass ich mit „Python“ die Referenzimplementierung des Python-Interpreters CPython meine. In diesem Material werden wir auf die anderen Implementierungen eingehen. Eigentlich möchte ich hier die Antwort auf die Frage finden, warum Python 2-10 mal mehr Zeit benötigt als andere Sprachen, um vergleichbare Probleme zu lösen, und ob es schneller geht.

Hier sind einige grundlegende Theorien, die versuchen zu erklären, warum Python langsam ist:

  • Der Grund dafür ist die GIL (Global Interpreter Lock, Global Interpreter Lock).
  • Der Grund ist, dass Python eher eine interpretierte als eine kompilierte Sprache ist.
  • Der Grund ist die dynamische Eingabe.

Wir werden diese Ideen analysieren und versuchen, die Antwort auf die Frage zu finden, was den größten Einfluss auf die Leistung von Python-Anwendungen hat.

Gil


Moderne Computer verfügen über Mehrkernprozessoren, und manchmal werden Multiprozessorsysteme gefunden. Um all diese Rechenleistung zu nutzen, verwendet das Betriebssystem Strukturen auf niedriger Ebene, die als Threads bezeichnet werden, während Prozesse (z. B. der Chrome-Browserprozess) viele Threads starten und entsprechend verwenden können. Wenn ein Prozess beispielsweise besonders dringend Prozessorressourcen benötigt, kann seine Ausführung auf mehrere Kerne aufgeteilt werden, sodass die meisten Anwendungen die Aufgaben, denen sie gegenüberstehen, schneller lösen können.

Zum Beispiel hat mein Chrome-Browser zum Zeitpunkt des Schreibens 44 offene Threads. Es ist zu beachten, dass die Struktur und API des Systems für die Arbeit mit Streams in Posix-basierten Betriebssystemen (Mac OS, Linux) und in der Windows-Betriebssystemfamilie unterschiedlich ist. Das Betriebssystem plant auch Threads.

Wenn Sie noch nie mit Multithread-Programmierung vertraut waren, müssen Sie sich jetzt mit den sogenannten Locks (Locks) vertraut machen. Sperren haben die Bedeutung, dass Sie ein solches Systemverhalten sicherstellen können, wenn in einer Umgebung mit mehreren Threads, z. B. wenn eine bestimmte Variable im Speicher geändert wird, mehrere Threads nicht auf denselben Speicherbereich zugreifen können (zum Lesen oder Ändern).

Wenn der CPython-Interpreter die Variablen erstellt, weist er Speicher zu und zählt dann die Anzahl der vorhandenen Verweise auf diese Variablen. Dieses Konzept wird als Referenzzählung bezeichnet. Wenn die Anzahl der Verbindungen gleich Null ist, wird der entsprechende Speicherplatz freigegeben. Aus diesem Grund führt beispielsweise die Erstellung von "temporären" Variablen, beispielsweise im Rahmen von Schleifen, nicht zu einer übermäßigen Erhöhung des von der Anwendung verbrauchten Arbeitsspeichers.

Der interessanteste Teil beginnt, wenn mehrere Threads dieselben Variablen verwenden. Das Hauptproblem hierbei ist, wie genau CPython die Referenzzählung durchführt. Hier wird die Aktion der "globalen Interpretersperre" angezeigt, die die Ausführung von Threads sorgfältig steuert.

Ein Interpreter kann jeweils nur eine Operation ausführen, unabhängig davon, wie viele Threads sich im Programm befinden.

▍Wie wirkt sich GIL auf die Leistung von Python-Anwendungen aus?


Wenn eine Single-Threaded-Anwendung im selben Python-Interpreter-Prozess ausgeführt wird, wirkt sich die GIL in keiner Weise auf die Leistung aus. Wenn Sie beispielsweise GIL loswerden, werden wir keinen Leistungsunterschied feststellen.

Wenn im Rahmen eines Python-Interpreter-Prozesses eine parallele Datenverarbeitung mithilfe von Multithreading-Mechanismen implementiert werden muss und die verwendeten Streams das E / A-Subsystem intensiv nutzen (z. B. wenn sie mit einem Netzwerk oder einer Festplatte arbeiten), können die Folgen von beobachtet werden wie GIL Threads verwaltet. So sieht es aus, wenn zwei Threads verwendet werden und Prozesse intensiv geladen werden.


GIL-Visualisierung ( von hier aus )

Wenn Sie eine Webanwendung haben (zum Beispiel basierend auf dem Django-Framework) und WSGI verwenden, wird jede Anforderung für die Webanwendung von einem separaten Python-Interpreter-Prozess bearbeitet, dh wir haben nur eine Anforderungssperre. Da der Python-Interpreter langsam startet, gibt es in einigen WSGI-Implementierungen einen sogenannten "Daemon-Modus", bei dem die Interpreter-Prozesse in einem funktionierenden Zustand gehalten werden, wodurch das System Anforderungen schneller bearbeiten kann.

▍Wie verhalten sich andere Python-Interpreter?


PyPy hat eine GIL, es ist normalerweise mehr als dreimal schneller als CPython.

In Jython gibt es keine GIL, da Python-Threads in Jython als Java-Threads dargestellt werden. Solche Threads verwenden die Speicherverwaltungsfunktionen der JVM.

▍Wie ist die Flusskontrolle in JavaScript organisiert?


Wenn wir über JavaScript sprechen, sollte zunächst beachtet werden, dass alle JS-Engines den Mark-and-Sweep- Garbage-Collection-Algorithmus verwenden. Wie bereits erwähnt, ist der Hauptgrund für die Verwendung von GIL der in CPython verwendete Speicherverwaltungsalgorithmus.

JavaScript hat keine GIL, JS ist jedoch eine Single-Threaded-Sprache und benötigt daher keinen solchen Mechanismus. Anstelle der parallelen Codeausführung verwendet JavaScript asynchrone Programmiertechniken, die auf einer Ereignisschleife, Versprechungen und Rückrufen basieren. Python hat etwas Ähnliches vom asyncio Modul asyncio .

Python - interpretierte Sprache


Ich habe oft gehört, dass die schlechte Leistung von Python auf die Tatsache zurückzuführen ist, dass es sich um eine interpretierte Sprache handelt. Solche Aussagen basieren auf einer groben Vereinfachung der tatsächlichen Funktionsweise von CPython. Wenn Sie im Terminal einen Befehl wie python myscript.py , beginnt CPython mit einer langen Abfolge von Aktionen, die aus Lesen, lexikalischer Analyse, Parsen, Kompilieren, Interpretieren und Ausführen von python myscript.py besteht. Wenn Sie an den Details interessiert sind, schauen Sie sich dieses Material an.

Wenn wir diesen Prozess betrachten, ist es für uns besonders wichtig, dass hier in der Kompilierungsphase eine .pyc Datei erstellt wird und eine Folge von Bytecodes in die Datei im __pycache__/ , die sowohl in Python 3 als auch in Python verwendet wird 2.

Dies gilt nicht nur für von uns geschriebene Skripte, sondern auch für importierten Code, einschließlich Module von Drittanbietern.

Infolgedessen führt Python die meiste Zeit (es sei denn, Sie schreiben Code, der nur einmal ausgeführt wird) den fertigen Bytecode aus. Vergleicht man dies mit den Vorgängen in Java und C #, so stellt sich heraus, dass der Java-Code in die „Intermediate Language“ kompiliert wird und die virtuelle Java-Maschine den Bytecode liest und ihre JIT-Kompilierung in Maschinencode durchführt. Die "Zwischensprache" .NET CIL (die mit der .NET Common-Language-Runtime, CLR identisch ist) verwendet die JIT-Kompilierung, um zum Maschinencode zu navigieren.

Infolgedessen wird sowohl in Java als auch in C # eine „Zwischensprache“ verwendet, und ähnliche Mechanismen sind vorhanden. Warum zeigt Python dann viel schlechtere Benchmarks als Java und C #, wenn alle diese Sprachen virtuelle Maschinen und eine Art Bytecode verwenden? Zunächst aufgrund der Tatsache, dass die JIT-Kompilierung in .NET und Java verwendet wird.

Die JIT-Kompilierung (Just In Time-Kompilierung, On-the-Fly- oder Just-in-Time-Kompilierung) erfordert eine Zwischensprache, um die Aufteilung des Codes in Fragmente (Frames) zu ermöglichen. AOT-Kompilierungssysteme (Ahead Of Time-Kompilierung, Kompilierung vor der Ausführung) sind so konzipiert, dass die volle Funktionalität des Codes sichergestellt ist, bevor die Interaktion dieses Codes mit dem System beginnt.

Die Verwendung von JIT beschleunigt die Ausführung des Codes nicht, da einige Fragmente des Bytecodes wie in Python ausgeführt werden. Mit JIT können Sie jedoch während der Ausführung Codeoptimierungen durchführen. Ein guter JIT-Optimierer kann die am meisten geladenen Teile der Anwendung identifizieren (dieser Teil der Anwendung wird als „Hot Spot“ bezeichnet) und die entsprechenden Codefragmente optimieren, indem er sie durch optimierte und produktivere Optionen als die zuvor verwendeten ersetzt.

Dies bedeutet, dass eine solche Optimierung die Ausführung solcher Aktionen erheblich beschleunigen kann, wenn eine bestimmte Anwendung bestimmte Aktionen immer wieder ausführt. Beachten Sie auch, dass Java und C # stark typisierte Sprachen sind, damit der Optimierer mehr Annahmen über Code treffen kann, die zur Verbesserung der Programmleistung beitragen können.

In PyPy gibt es einen JIT-Compiler, und wie bereits erwähnt, ist diese Python-Interpreter-Implementierung viel schneller als CPython. Informationen zum Vergleichen verschiedener Python-Interpreter finden Sie in diesem Artikel.

▍ Warum verwendet CPython keinen JIT-Compiler?


JIT-Compiler haben auch Nachteile. Eine davon ist die Startzeit. CPython startet bereits relativ langsam und PyPy ist 2-3 mal langsamer als CPython. Die lange Laufzeit der JVM ist ebenfalls bekannt. CLR .NET umgeht dieses Problem, indem es während des Systemstarts gestartet wird. Es ist jedoch zu beachten, dass sowohl die CLR als auch das Betriebssystem, auf dem die CLR ausgeführt wird, von derselben Firma entwickelt wurden.

Wenn Sie einen Python-Prozess haben, der schon lange ausgeführt wird, während in einem solchen Prozess Code optimiert werden kann, da er häufig verwendete Abschnitte enthält, sollten Sie sich ernsthaft einen Interpreter mit einem JIT-Compiler ansehen.

CPython ist jedoch eine Implementierung des Allzweck-Python-Interpreters. Wenn Sie also mit Python eine Befehlszeilenanwendung entwickeln, wird die Arbeit erheblich verlangsamt, wenn der JIT-Compiler bei jedem Start dieser Anwendung lange warten muss.

CPython versucht, so viele Python-Anwendungsfälle wie möglich zu unterstützen. Beispielsweise besteht die Möglichkeit, den JIT-Compiler mit Python zu verbinden. Das Projekt , das diese Idee umsetzt, entwickelt sich jedoch nicht sehr aktiv.

Daher können wir sagen, dass Sie den PyPy-Interpreter verwenden, wenn Sie Python zum Schreiben eines Programms verwenden, dessen Leistung sich bei Verwendung des JIT-Compilers verbessern kann.

Python ist eine dynamisch typisierte Sprache


In statisch typisierten Sprachen müssen Sie beim Deklarieren von Variablen deren Typen angeben. Unter diesen Sprachen können C, C ++, Java, C #, Go notiert werden.

In dynamisch typisierten Sprachen hat das Konzept eines Datentyps dieselbe Bedeutung, aber der Typ einer Variablen ist dynamisch.

 a = 1 a = "foo" 

In diesem einfachsten Beispiel erstellt Python zuerst die erste Variable a , dann die zweite mit demselben Namen vom Typ str und gibt den Speicher frei, der der ersten Variablen a zugewiesen wurde.

Es mag den Anschein haben, dass das Schreiben in Sprachen mit dynamischer Typisierung bequemer und einfacher ist als in Sprachen mit statischer Typisierung. Solche Sprachen wurden jedoch nicht aus einer Laune heraus erstellt. Bei ihrer Entwicklung wurden die Merkmale von Computersystemen berücksichtigt. Alles, was am Ende im Programmtext geschrieben steht, hängt von den Anweisungen des Prozessors ab. Dies bedeutet, dass die vom Programm verwendeten Daten, beispielsweise in Form von Objekten oder anderen Datentypen, auch in Strukturen auf niedriger Ebene konvertiert werden.

Python führt solche Transformationen automatisch durch, der Programmierer sieht diese Prozesse nicht und muss sich nicht um solche Transformationen kümmern.

Wenn Sie den Typ einer Variablen nicht angeben müssen, wenn Sie sie deklarieren, ist dies keine Funktion der Sprache, die Python langsam macht. Die Spracharchitektur ermöglicht es, fast alles dynamisch zu machen. Zur Laufzeit können Sie beispielsweise Objektmethoden ersetzen. Auch hier können Sie während der Ausführung des Programms die "Monkey Patch" -Technik verwenden, die auf Systemaufrufe auf niedriger Ebene angewendet wird. In Python ist fast alles möglich.

Es ist die Python-Architektur, die die Optimierung extrem schwierig macht.

Um diese Idee zu veranschaulichen, werde ich ein Tool zum Verfolgen von Systemaufrufen unter MacOS namens DTrace verwenden.

In der fertigen CPython-Distribution gibt es keine DTrace-Unterstützungsmechanismen, daher muss CPython mit den entsprechenden Einstellungen neu kompiliert werden. Hier wird Version 3.6.6 verwendet. Wir verwenden also die folgende Abfolge von Aktionen:

 wget https://github.com/python/cpython/archive/v3.6.6.zip unzip v3.6.6.zip cd v3.6.6 ./configure --with-dtrace make 

Mit python.exe können Sie jetzt DTRace verwenden, um den Code zu verfolgen. Lesen Sie hier, wie Sie DTrace mit Python verwenden. Und hier finden Sie Skripte zum Messen verschiedener Leistungsindikatoren von Python-Programmen mit DTrace. Darunter befinden sich Parameter zum Aufrufen von Funktionen, zur Laufzeit von Programmen, zur Zeit der Prozessorauslastung, zu Informationen über Systemaufrufe usw. So verwenden Sie den Befehl dtrace :

 sudo dtrace -s toolkit/<tracer>.d -c '../cpython/python.exe script.py' 

Und so zeigt die Trace-Funktion py_callflow Funktionsaufrufe in der Anwendung an.


Ablaufverfolgung mit DTrace

Beantworten wir nun die Frage, ob sich die dynamische Eingabe auf die Python-Leistung auswirkt. Hier einige Gedanken dazu:

  • Typprüfung und Konvertierung sind schwere Vorgänge. Jedes Mal, wenn auf eine Variable zugegriffen, diese gelesen oder geschrieben wird, wird eine Typprüfung durchgeführt.
  • Eine Sprache mit einer solchen Flexibilität ist schwer zu optimieren. Der Grund dafür, dass andere Sprachen so viel schneller als Python sind, besteht darin, dass sie Kompromisse eingehen, indem sie zwischen Flexibilität und Leistung wählen.
  • Das Cython- Projekt kombiniert Python und statische Typisierung, was beispielsweise, wie in diesem Artikel gezeigt , zu einer 84-fachen Leistungsverbesserung gegenüber regulärem Python führt. Schauen Sie sich dieses Projekt an, wenn Sie Geschwindigkeit benötigen.

Zusammenfassung


Der Grund für die schlechte Leistung von Python ist seine Dynamik und Vielseitigkeit. Es kann als Werkzeug zur Lösung einer Vielzahl von Aufgaben verwendet werden. Um die gleichen Ziele zu erreichen, können Sie versuchen, nach produktiveren und besser optimierten Tools zu suchen. Vielleicht können sie finden, vielleicht auch nicht.

In Python geschriebene Anwendungen können mithilfe der Funktionen der asynchronen Codeausführung, der Profilerstellungstools und der Auswahl des richtigen Interpreters optimiert werden. Um die Geschwindigkeit von Anwendungen zu optimieren, deren Startzeit unwichtig ist und deren Leistung von der Verwendung des JIT-Compilers profitieren kann, sollten Sie PyPy verwenden. Wenn Sie maximale Leistung benötigen und auf die Einschränkungen der statischen Typisierung vorbereitet sind, schauen Sie sich Cython an.

Liebe Leser! Wie lösen Sie schlechte Python-Leistungsprobleme?

Source: https://habr.com/ru/post/de418823/


All Articles