Angenommen, Ihr Python-Programm ist langsam und Sie stellen fest, dass dies
nur teilweise auf einen Mangel an Prozessorressourcen zurückzuführen ist . Wie finde ich heraus, welche Teile des Codes gezwungen sind, etwas zu erwarten, das nicht für die CPU gilt?

Nachdem Sie das Material gelesen haben, dessen Übersetzung wir heute veröffentlichen, lernen Sie, wie Sie Ihre eigenen Profiler für Python-Code schreiben. Es handelt sich um Tools, mit denen inaktive Stellen im Code erkannt werden, während auf die Freigabe bestimmter Ressourcen gewartet wird. Insbesondere werden wir hier Folgendes diskutieren:
- Was kann das Programm erwarten?
- Profilierung der Verwendung von Ressourcen, die keine CPU-Ressourcen sind.
- Profilerstellung für unbeabsichtigte Kontextwechsel.
Was erwartet das Programm?
In jenen Momenten, in denen das Programm nicht mit intensiven Berechnungen unter Verwendung des Prozessors beschäftigt ist, scheint es auf etwas zu warten. Dies kann zu Untätigkeit des Programms führen:
- Netzwerkressourcen. Dies kann das Warten auf den Abschluss von DNS-Suchvorgängen, das Warten auf eine Antwort von einer Netzwerkressource, das Warten auf das Ende des Ladens einiger Daten usw. umfassen.
- Festplatte. Das Lesen von Daten von der Festplatte kann einige Zeit dauern. Gleiches gilt für das Schreiben auf die Festplatte. Manchmal werden Lese- oder Schreibvorgänge nur mit einem im RAM befindlichen Cache ausgeführt. Mit diesem Ansatz geht alles ziemlich schnell. Wenn ein Programm jedoch direkt mit einer Festplatte interagiert, sind solche Vorgänge manchmal recht langsam.
- Schlösser. Ein Programm kann warten, um einen Thread oder Prozess zu entsperren.
- Aussetzung der Arbeit. Manchmal kann ein Programm die Arbeit absichtlich unterbrechen, z. B. zwischen Versuchen, eine Aktion auszuführen.
Wie finde ich Orte von Programmen, an denen etwas passiert, das die Leistung stark beeinträchtigt?
Methode Nr. 1: Analyse der Zeit, in der das Programm den Prozessor nicht verwendet
Der in Python integrierte Profiler
cProfile
kann Daten zu vielen verschiedenen Indikatoren sammeln, die sich auf den Betrieb von Programmen beziehen. Aus diesem Grund kann damit ein Tool erstellt werden, mit dem Sie die Zeit analysieren können, in der das Programm keine Prozessorressourcen verwendet.
Das Betriebssystem kann uns genau
sagen , wie viel Prozessorzeit das Programm verwendet hat.
Stellen Sie sich vor, wir erstellen ein Single-Thread-Programm. Multithread-Programme sind schwieriger zu profilieren, und die Beschreibung dieses Prozesses ist ebenfalls nicht einfach. Wenn das Programm 9 Sekunden lang ausgeführt wurde und gleichzeitig den Prozessor 7,5 Sekunden lang verwendet wurde, bedeutet dies, dass 1,5 Sekunden gewartet wurde.
Erstellen Sie zunächst einen Timer, der das Timeout misst:
import os def not_cpu_time(): times = os.times() return times.elapsed - (times.system + times.user)
Erstellen Sie dann einen Profiler, der diese Zeit analysiert:
import cProfile, pstats def profile_not_cpu_time(f, *args, **kwargs): prof = cProfile.Profile(not_cpu_time) prof.runcall(f, *args, **kwargs) result = pstats.Stats(prof) result.sort_stats("time") result.print_stats()
Danach können Sie verschiedene Funktionen profilieren:
>>> profile_not_cpu_time( ... lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function) 3 0.050 0.017 _ssl._SSLSocket.read 1 0.040 0.040 _socket.getaddrinfo 1 0.020 0.020 _socket.socket.connect 1 0.010 0.010 _ssl._SSLSocket.do_handshake 342 0.010 0.000 find.str 192 0.010 0.000 append.list
Die Ergebnisse lassen den Schluss zu, dass die meiste Zeit mit dem Lesen von Daten aus dem Socket verbracht wurde. Es dauerte jedoch einige Zeit, um eine DNS-Suche (
getaddrinfo
) sowie einen TCP-Handshake (
connect
) und einen TLS / SSL-Handshake durchzuführen.
Da wir uns bemüht haben, die Zeiträume des Programmbetriebs zu untersuchen, in denen keine Prozessorressourcen verwendet werden, wissen wir, dass dies alles reine Wartezeit ist, dh die Zeit, in der das Programm nicht mit Berechnungen beschäftigt ist.
Warum wird Zeit für
str.find
und
list.append
? Bei der Ausführung solcher Operationen muss das Programm nicht warten, daher erscheint die Erklärung plausibel, wonach es sich um eine Situation handelt, in der nicht der gesamte Prozess ausgeführt wurde. Vielleicht - auf den Abschluss eines anderen Prozesses warten oder auf den Abschluss des Ladens von Daten aus der Auslagerungsdatei in den Speicher. Dies zeigt an, dass einige Zeit für die Ausführung dieser Vorgänge aufgewendet wurde, was nicht Teil der Prozessorzeit ist.
Außerdem möchte ich darauf hinweisen, dass ich Berichte gesehen habe, die kleine negative Zeitfragmente enthalten. Dies deutet auf eine gewisse Diskrepanz zwischen der verstrichenen Zeit und der Prozessorzeit hin, aber ich erwarte nicht, dass dies einen signifikanten Einfluss auf die Analyse komplexerer Programme hat.
Methode Nummer 2: Analyse der Anzahl der absichtlichen Kontextwechsel
Das Problem bei der Messung der Zeit, die das Programm für das Warten auf etwas benötigt, besteht darin, dass bei der Durchführung verschiedener Messsitzungen für dasselbe Programm die Zeit variieren kann, die außerhalb des Programmumfangs liegt. Manchmal können DNS-Abfragen langsamer als gewöhnlich sein. Manchmal werden einige Daten langsamer als gewöhnlich geladen. Daher wäre es nützlich, einige besser vorhersehbare Indikatoren zu verwenden, die nicht an die Geschwindigkeit der Umgebung des Programms gebunden sind.
Eine Möglichkeit, dies zu tun, besteht darin, zu berechnen, wie viele Operationen, die warten müssen, den Prozess abgeschlossen haben. Das heißt, wir sprechen über die Berechnung der Anzahl der Wartezeiten und nicht über die Zeit, die für das Warten auf etwas aufgewendet wird.
Ein Prozess kann die Verwendung von Prozessorressourcen aus zwei Gründen einstellen:
- Jedes Mal, wenn ein Prozess eine Operation ausführt, die nicht sofort beendet wird, z. B. Daten aus einem Socket liest, pausiert usw., entspricht dies den Angaben zum Betriebssystem: "Weck mich auf, wenn ich weiterarbeiten kann." Dies ist der sogenannte „absichtliche Kontextwechsel“: Der Prozessor kann zu einem anderen Prozess wechseln, bis die Daten auf dem Socket angezeigt werden oder bis unser Prozess den Standby-Modus verlässt, sowie in anderen ähnlichen Fällen.
- "Unbeabsichtigtes Umschalten des Kontexts" ist eine Situation, in der das Betriebssystem einen Prozess vorübergehend stoppt, sodass ein anderer Prozess die Prozessorressourcen nutzen kann.
Wir werden absichtliche Kontextwechsel profilieren.
Schreiben wir einen Profiler, der absichtliche Kontextwechsel mithilfe der
psutil
Bibliothek zählt:
import psutil _current_process = psutil.Process() def profile_voluntary_switches(f, *args, **kwargs): prof = cProfile.Profile( lambda: _current_process.num_ctx_switches().voluntary) prof.runcall(f, *args, **kwargs) result = pstats.Stats(prof) result.sort_stats("time") result.print_stats()
Lassen Sie uns nun den Code profilieren, der wieder mit dem Netzwerk funktioniert:
>>> profile_voluntary_switches( ... lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function) 3 7.000 2.333 _ssl._SSLSocket.read 1 2.000 2.000 _ssl._SSLSocket.do_handshake 1 2.000 2.000 _socket.getaddrinfo 1 1.000 1.000 _ssl._SSLContext.set_default_verify_path 1 1.000 1.000 _socket.socket.connect
Anstelle von Wartezeitdaten können wir jetzt Informationen über die Anzahl der beabsichtigten Kontextwechsel anzeigen, die aufgetreten sind.
Beachten Sie, dass Sie manchmal absichtliche Kontextwechsel an unerwarteten Stellen sehen können. Ich glaube, dies passiert, wenn Daten aus der Auslagerungsdatei aufgrund von Speicherseitenfehlern geladen werden.
Zusammenfassung
Die Verwendung der hier beschriebenen Code-Profiling-Technik führt zu einer gewissen zusätzlichen Belastung des Systems, die das Programm erheblich verlangsamt. In den meisten Fällen sollte dies jedoch nicht zu einer signifikanten Verzerrung der Ergebnisse führen, da wir den Einsatz von Prozessorressourcen nicht analysieren.
Im Allgemeinen kann festgestellt werden, dass sich messbare Indikatoren im Zusammenhang mit der Arbeit des Programms für die Profilerstellung eignen. Zum Beispiel Folgendes:
- Die Anzahl der
psutil.Process().read_count
( psutil.Process().read_count
) und Schreibvorgänge ( psutil.Process().write_count
). - Unter Linux die Gesamtzahl der gelesenen und geschriebenen Bytes (psutil.
Process().read_chars
). - Speicherzuweisungsindikatoren (die Durchführung einer solchen Analyse erfordert einige Anstrengungen; dies kann mit jemalloc erfolgen ).
Details zu den ersten beiden Elementen dieser Liste finden Sie in der
psutil- Dokumentation.
Liebe Leser! Wie profilieren Sie Ihre Python-Anwendungen?
