Ich habe neulich an einem Drehbuch gearbeitet und es hat mich wahnsinnig gemacht. Es hat zwar funktioniert, aber es conflict nur … langsam. Wirklich langsam. Ich hatte das Gefühl, dass es viel schneller gehen könnte, wenn ich es herausfinden könnte Wo die Verzögerung conflict.
Mein erster Gedanke conflict, mit den Optimierungen zu beginnen. Ich konnte das Laden der Daten optimieren. Oder die for-Schleife umschreiben? Aber ich habe mich zurückgehalten. Ich bin schon einmal in diese Falle getappt und habe Stunden damit verbracht, einen Codeabschnitt zu „optimieren“, nur um dann festzustellen, dass er kaum einen Unterschied zur Gesamtlaufzeit macht. Donald Knuth hatte Recht, als er sagte: „Vorzeitige Optimierung ist die Wurzel allen Übels.“
Ich habe mich für einen methodischeren Ansatz entschieden. Anstatt zu raten, wollte ich es sicher herausfinden. Ich musste ein Profil des Codes erstellen, um konkrete Daten darüber zu erhalten, welche Funktionen genau die meisten Taktzyklen verbrauchten.
In diesem Artikel werde ich Sie durch den genauen Prozess führen, den ich verwendet habe. Wir nehmen ein bewusst langsames Python-Skript und verwenden zwei fantastische Instruments, um seine Engpässe mit chirurgischer Präzision zu lokalisieren.
Das erste dieser Instruments heißt cProfileein leistungsstarker, in Python integrierter Profiler. Der andere heißt Schlangenvisz, A geniales Werkzeug Das wandelt die Ausgabe des Profilers in eine interaktive visuelle Karte um.
Einrichten einer Entwicklungsumgebung
Bevor wir mit dem Codieren beginnen, richten wir unsere Entwicklungsumgebung ein. Die beste Vorgehensweise besteht darin, eine separate Python-Umgebung zu erstellen, in der Sie die erforderliche Software program installieren und experimentieren können, in dem Wissen, dass alles, was Sie tun, keine Auswirkungen auf den Relaxation Ihres Techniques hat. Ich werde dafür Conda verwenden, aber Sie können jede Methode verwenden, mit der Sie vertraut sind.
#create our check setting
conda create -n profiling_lab python=3.11 -y
# Now activate it
conda activate profiling_lab
Nachdem wir nun unsere Umgebung eingerichtet haben, müssen wir Snakeviz für unsere Visualisierungen und Numpy für das Beispielskript installieren. cProfile ist bereits in Python enthalten, daher gibt es dort nichts weiter zu tun. Da wir unsere Skripte mit einem Jupyter-Pocket book ausführen, werden wir dieses auch installieren.
# Set up our visualization instrument and numpy
pip set up snakeviz numpy jupyter
Geben Sie nun ein jupyter pocket book in Ihre Eingabeaufforderung ein. In Ihrem Browser sollte ein Jupiter-Notizbuch geöffnet sein. Wenn dies nicht automatisch geschieht, wird Ihnen danach wahrscheinlich ein Bildschirm voller Informationen angezeigt jupyter pocket book Befehl. Ganz unten finden Sie eine URL, die Sie kopieren und in Ihren Browser einfügen sollten, um das Jupyter Pocket book zu starten.
Ihre URL wird sich von meiner unterscheiden, aber sie sollte in etwa so aussehen:-
http://127.0.0.1:8888/tree?token=3b9f7bd07b6966b41b68e2350721b2d0b6f388d248cc69da
Nachdem unsere Instruments bereit sind, ist es an der Zeit, einen Blick auf den Code zu werfen, den wir reparieren werden.
Unser „Drawback“-Skript
Um unsere Profiling-Instruments ordnungsgemäß zu testen, benötigen wir ein Skript, das eindeutige Leistungsprobleme aufweist. Ich habe ein einfaches Programm geschrieben, das Verarbeitungsprobleme mit Speicher, Iteration und CPU-Zyklen simuliert, was es zu einem perfekten Kandidaten für unsere Untersuchung macht.
# run_all_systems.py
import time
import math
# ===================================================================
CPU_ITERATIONS = 34552942
STRING_ITERATIONS = 46658100
LOOP_ITERATIONS = 171796964
# ===================================================================
# --- Activity 1: A Calibrated CPU-Certain Bottleneck ---
def cpu_heavy_task(iterations):
print(" -> Operating CPU-bound job...")
end result = 0
for i in vary(iterations):
end result += math.sin(i) * math.cos(i) + math.sqrt(i)
return end result
# --- Activity 2: A Calibrated Reminiscence/String Bottleneck ---
def memory_heavy_string_task(iterations):
print(" -> Operating Reminiscence/String-bound job...")
report = ""
chunk = "report_item_abcdefg_123456789_"
for i in vary(iterations):
report += f"|{chunk}{i}"
return report
# --- Activity 3: A Calibrated "Thousand Cuts" Iteration Bottleneck ---
def simulate_tiny_op(n):
move
def iteration_heavy_task(iterations):
print(" -> Operating Iteration-bound job...")
for i in vary(iterations):
simulate_tiny_op(i)
return "OK"
# --- Fundamental Orchestrator ---
def run_all_systems():
print("--- Beginning FINAL SLOW Balanced Showcase ---")
cpu_result = cpu_heavy_task(iterations=CPU_ITERATIONS)
string_result = memory_heavy_string_task(iterations=STRING_ITERATIONS)
iteration_result = iteration_heavy_task(iterations=LOOP_ITERATIONS)
print("--- FINAL SLOW Balanced Showcase Completed ---")
Schritt 1: Sammeln der Daten mit cProfile
Unser erstes Software, cProfile, ist ein in Python integrierter deterministischer Profiler. Wir können es über den Code ausführen, um unser Skript auszuführen und detaillierte Statistiken zu jedem Funktionsaufruf aufzuzeichnen.
import cProfile, pstats, io
pr = cProfile.Profile()
pr.allow()
# Run the perform you wish to profile
run_all_systems()
pr.disable()
# Dump stats to a string and print the highest 10 by cumulative time
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats("cumtime")
ps.print_stats(10)
print(s.getvalue())
Hier ist die Ausgabe.
--- Beginning FINAL SLOW Balanced Showcase ---
-> Operating CPU-bound job...
-> Operating Reminiscence/String-bound job...
-> Operating Iteration-bound job...
--- FINAL SLOW Balanced Showcase Completed ---
275455984 perform calls in 30.497 seconds
Ordered by: cumulative time
Listing diminished from 47 to 10 resulting from restriction <10>
ncalls tottime percall cumtime percall filename:lineno(perform)
2 0.000 0.000 30.520 15.260 /house/tom/.native/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3541(run_code)
2 0.000 0.000 30.520 15.260 {built-in technique builtins.exec}
1 0.000 0.000 30.497 30.497 /tmp/ipykernel_173802/1743829582.py:41(run_all_systems)
1 9.652 9.652 14.394 14.394 /tmp/ipykernel_173802/1743829582.py:34(iteration_heavy_task)
1 7.232 7.232 12.211 12.211 /tmp/ipykernel_173802/1743829582.py:14(cpu_heavy_task)
171796964 4.742 0.000 4.742 0.000 /tmp/ipykernel_173802/1743829582.py:31(simulate_tiny_op)
1 3.891 3.891 3.892 3.892 /tmp/ipykernel_173802/1743829582.py:22(memory_heavy_string_task)
34552942 1.888 0.000 1.888 0.000 {built-in technique math.sin}
34552942 1.820 0.000 1.820 0.000 {built-in technique math.cos}
34552942 1.271 0.000 1.271 0.000 {built-in technique math.sqrt}
Wir haben eine Reihe von Zahlen, die schwer zu interpretieren sein können. Hier kommt Snakeviz ins Spiel.
Schritt 2: Visualisierung des Engpasses mit Snakeviz
Hier geschieht die Magie. Snakeviz nimmt die Ausgabe unserer Profilierungsdatei und wandelt sie in ein interaktives, browserbasiertes Diagramm um, um das Auffinden von Engpässen zu erleichtern.
Nutzen wir additionally dieses Software, um zu visualisieren, was wir haben. Da ich ein Jupyter-Pocket book verwende, müssen wir es zuerst laden.
%load_ext snakeviz
Und wir führen es so aus.
%%snakeviz
predominant()
Die Ausgabe besteht aus zwei Teilen. Zuerst ist eine Visualisierung wie diese.

Was Sie sehen, ist ein „Eiszapfen“-Diagramm von oben nach unten. Von oben nach unten stellt es die Anrufhierarchie dar.
Ganz oben: Python führt unser Skript aus (
Als nächstes: die __main__-Ausführung des Skripts (
Der speicherintensive Verarbeitungsteil ist im Diagramm nicht gekennzeichnet. Das liegt daran, dass der Zeitanteil, der mit dieser Aufgabe verbunden ist, viel kleiner ist als der Zeitanteil, der auf die anderen beiden intensiven Funktionen entfällt. Als Ergebnis sehen wir rechts vom cpu_heavy_task-Block einen viel kleineren, unbeschrifteten Block.
Beachten Sie, dass es für die Analyse auch einen Snakeviz-Diagrammstil namens a gibt Sunburst-Diagramm. Es ähnelt ein wenig einem Kreisdiagramm, enthält jedoch eine Reihe immer größerer konzentrischer Kreise und Bögen. Die Idee dahinter ist, dass die Zeit, die Funktionen zum Ausführen benötigen, durch die Winkelausdehnung der Bogengröße des Kreises dargestellt wird. Die Wurzelfunktion ist ein Kreis in der Mitte von nämlich. Die Root-Funktion wird ausgeführt, indem sie die darunter liegenden Unterfunktionen usw. aufruft. Wir werden uns in diesem Artikel nicht mit diesem Anzeigetyp befassen.
Eine solche visuelle Bestätigung kann so viel wirkungsvoller sein, als auf eine Zahlentabelle zu starren. Ich musste nicht mehr raten, wo ich suchen sollte; Die Daten starrten mich direkt ins Gesicht.
Auf die Visualisierung folgt schnell ein Textblock, der die Zeitabläufe für verschiedene Teile Ihres Codes detailliert beschreibt, ähnlich wie die Ausgabe des cprofile-Instruments. Ich zeige hier nur die ersten etwa ein Dutzend Zeilen, da es insgesamt über 30 waren.
ncalls tottime percall cumtime percall filename:lineno(perform)
----------------------------------------------------------------
1 9.581 9.581 14.3 14.3 1062495604.py:34(iteration_heavy_task)
1 7.868 7.868 12.92 12.92 1062495604.py:14(cpu_heavy_task)
171796964 4.717 2.745e-08 4.717 2.745e-08 1062495604.py:31(simulate_tiny_op)
1 3.848 3.848 3.848 3.848 1062495604.py:22(memory_heavy_string_task)
34552942 1.91 5.527e-08 1.91 5.527e-08 ~:0(<built-in technique math.sin>)
34552942 1.836 5.313e-08 1.836 5.313e-08 ~:0(<built-in technique math.cos>)
34552942 1.305 3.778e-08 1.305 3.778e-08 ~:0(<built-in technique math.sqrt>)
1 0.02127 0.02127 31.09 31.09 <string>:1(<module>)
4 0.0001764 4.409e-05 0.0001764 4.409e-05 socket.py:626(ship)
10 0.000123 1.23e-05 0.0004568 4.568e-05 iostream.py:655(write)
4 4.594e-05 1.148e-05 0.0002735 6.838e-05 iostream.py:259(schedule)
...
...
...
Schritt 3: Die Lösung
Instruments wie Cprofiler und Snakeviz verraten es Ihnen natürlich nicht Wie um Ihre Leistungsprobleme zu beheben, aber da ich nun genau wusste, wo die Probleme lagen, konnte ich gezielte Korrekturen vornehmen.
# final_showcase_fixed_v2.py
import time
import math
import numpy as np
# ===================================================================
CPU_ITERATIONS = 34552942
STRING_ITERATIONS = 46658100
LOOP_ITERATIONS = 171796964
# ===================================================================
# --- Repair 1: Vectorization for the CPU-Certain Activity ---
def cpu_heavy_task_fixed(iterations):
"""
Fastened by utilizing NumPy to carry out the advanced math on a whole array
without delay, in extremely optimized C code as an alternative of a Python loop.
"""
print(" -> Operating CPU-bound job...")
# Create an array of numbers from 0 to iterations-1
i = np.arange(iterations, dtype=np.float64)
# The identical calculation, however vectorized, is orders of magnitude quicker
result_array = np.sin(i) * np.cos(i) + np.sqrt(i)
return np.sum(result_array)
# --- Repair 2: Environment friendly String Becoming a member of ---
def memory_heavy_string_task_fixed(iterations):
"""
Fastened by utilizing an inventory comprehension and a single, environment friendly ''.be a part of() name.
This avoids creating hundreds of thousands of intermediate string objects.
"""
print(" -> Operating Reminiscence/String-bound job...")
chunk = "report_item_abcdefg_123456789_"
# A listing comprehension is quick and memory-efficient
elements = (f"|{chunk}{i}" for i in vary(iterations))
return "".be a part of(elements)
# --- Repair 3: Eliminating the "Thousand Cuts" Loop ---
def iteration_heavy_task_fixed(iterations):
"""
Fastened by recognizing the duty is usually a no-op or a bulk operation.
In a real-world state of affairs, you'd discover a strategy to keep away from the loop solely.
Right here, we show the repair by merely eradicating the pointless loop.
The aim is to indicate the price of the loop itself was the issue.
"""
print(" -> Operating Iteration-bound job...")
# The repair is to discover a bulk operation or eradicate the necessity for the loop.
# Because the unique perform did nothing, the repair is to do nothing, however quicker.
return "OK"
# --- Fundamental Orchestrator ---
def run_all_systems():
"""
The primary orchestrator now calls the FAST variations of the duties.
"""
print("--- Beginning FINAL FAST Balanced Showcase ---")
cpu_result = cpu_heavy_task_fixed(iterations=CPU_ITERATIONS)
string_result = memory_heavy_string_task_fixed(iterations=STRING_ITERATIONS)
iteration_result = iteration_heavy_task_fixed(iterations=LOOP_ITERATIONS)
print("--- FINAL FAST Balanced Showcase Completed ---")
Jetzt können wir den cprofiler für unseren aktualisierten Code erneut ausführen.
import cProfile, pstats, io
pr = cProfile.Profile()
pr.allow()
# Run the perform you wish to profile
run_all_systems()
pr.disable()
# Dump stats to a string and print the highest 10 by cumulative time
s = io.StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats("cumtime")
ps.print_stats(10)
print(s.getvalue())
#
# begin of output
#
--- Beginning FINAL FAST Balanced Showcase ---
-> Operating CPU-bound job...
-> Operating Reminiscence/String-bound job...
-> Operating Iteration-bound job...
--- FINAL FAST Balanced Showcase Completed ---
197 perform calls in 6.063 seconds
Ordered by: cumulative time
Listing diminished from 52 to 10 resulting from restriction <10>
ncalls tottime percall cumtime percall filename:lineno(perform)
2 0.000 0.000 6.063 3.031 /house/tom/.native/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3541(run_code)
2 0.000 0.000 6.063 3.031 {built-in technique builtins.exec}
1 0.002 0.002 6.063 6.063 /tmp/ipykernel_173802/1803406806.py:1(<module>)
1 0.402 0.402 6.061 6.061 /tmp/ipykernel_173802/3782967348.py:52(run_all_systems)
1 0.000 0.000 5.152 5.152 /tmp/ipykernel_173802/3782967348.py:27(memory_heavy_string_task_fixed)
1 4.135 4.135 4.135 4.135 /tmp/ipykernel_173802/3782967348.py:35(<listcomp>)
1 1.017 1.017 1.017 1.017 {technique 'be a part of' of 'str' objects}
1 0.446 0.446 0.505 0.505 /tmp/ipykernel_173802/3782967348.py:14(cpu_heavy_task_fixed)
1 0.045 0.045 0.045 0.045 {built-in technique numpy.arange}
1 0.000 0.000 0.014 0.014 <__array_function__ internals>:177(sum)
Das ist ein fantastisches Ergebnis, das die Leistungsfähigkeit der Profilerstellung demonstriert. Wir haben unsere Mühe auf die Teile des Codes gelenkt, die wichtig waren. Der Gründlichkeit halber habe ich auch Snakeviz für das korrigierte Skript ausgeführt.
%%snakeviz
run_all_systems()

Die bemerkenswerteste Änderung ist die Reduzierung der Gesamtlaufzeit von etwa 30 Sekunden auf etwa 6 Sekunden. Dies ist eine 5-fache Beschleunigung, die durch die Beseitigung der drei Hauptengpässe erreicht wird, die im „Vorher“-Profil sichtbar waren.
Schauen wir uns jedes einzeln an.
1. Die iteration_heavy_task
Vorher (Das Drawback)
Im ersten Bild ist der große Balken auf der linken Seite, iteration_heavy_task, der größte Engpass, der verbraucht 14,3 Sekunden.
- Warum conflict es langsam? Diese Aufgabe conflict ein klassischer „Tod durch tausend Schnitte“. Die Funktion simulieren_tiny_op hat quick nichts bewirkt, wurde aber millionenfach innerhalb einer reinen Python-for-Schleife aufgerufen. Der immense Aufwand, den der Python-Interpreter mit dem wiederholten Starten und Stoppen eines Funktionsaufrufs verursacht, conflict die alleinige Ursache für die Langsamkeit.
Die Lösung
Die feste Model iteration_heavy_task_fixed erkannte, dass das Ziel ohne die Schleife erreicht werden konnte. In unserem Fall bedeutete dies, die sinnlose Schleife vollständig zu entfernen. In einer realen Anwendung müsste dazu eine einzige „Massen“-Operation gefunden werden, um die iterative Operation zu ersetzen.
Nachher (Das Ergebnis)
Im zweiten Bild ist die iteration_heavy_task-Leiste zu sehen völlig verschwunden. Es ist jetzt so schnell, dass seine Laufzeit nur einen winzigen Bruchteil einer Sekunde beträgt und auf dem Diagramm unsichtbar ist. Wir haben ein 14,3-Sekunden-Drawback erfolgreich behoben.
2. Die cpu_heavy_task
Vorher (Das Drawback)
Der zweite große Engpass, deutlich sichtbar als großer orangefarbener Balken rechts, ist cpu_heavy_task, der in Anspruch genommen wurde 12,9 Sekunden.
- Warum conflict es langsam? Wie die Iterationsaufgabe conflict auch diese Funktion durch die Geschwindigkeit der Python-for-Schleife begrenzt. Während die darin enthaltenen mathematischen Operationen schnell waren, musste der Interpreter jede der Millionen Berechnungen einzeln verarbeiten, was für numerische Aufgaben äußerst ineffizient ist.
Die Lösung
Die Lösung conflict Vektorisierung Verwendung der NumPy-Bibliothek. Anstatt eine Python-Schleife zu verwenden, hat cpu_heavy_task_fixed ein NumPy-Array erstellt und alle mathematischen Operationen (np.sqrt, np.sin usw.) gleichzeitig für das gesamte Array ausgeführt. Diese Operationen werden in hochoptimiertem, vorkompiliertem C-Code ausgeführt, wobei die langsame Python-Interpreterschleife vollständig umgangen wird.
Nachher (Das Ergebnis).
Genau wie der erste Engpass ist auch die cpu_heavy_task-Leiste aus dem „Nachher“-Diagramm verschwunden. Seine Laufzeit wurde von 12,9 Sekunden auf wenige Millisekunden reduziert.
3. Die „memory_heavy_string_task“.
Vorher (Das Drawback):
Im ersten Diagramm lief die speicherintensive_string_task, aber ihre Laufzeit conflict im Vergleich zu den beiden anderen größeren Problemen gering, sodass sie in den kleinen, unbeschrifteten Bereich ganz rechts verbannt wurde. Es conflict ein relativ kleines Drawback.
Die Lösung
Die Lösung für diese Aufgabe bestand darin, das Ineffiziente zu ersetzen melden += „…” String-Verkettung mit einer viel effizienteren Methode: Erstellen einer Liste aller String-Teile und anschließender Aufruf „“.verbinden() ein einziges Mal am Ende.
Nachher (Das Ergebnis)
Im zweiten Diagramm sehen wir das Ergebnis unseres Erfolgs. Nachdem die beiden Engpässe von mehr als 10 Sekunden beseitigt wurden, ist nun die speicherintensive Zeichenfolgenaufgabe behoben neuer dominanter EngpassBuchhaltung 4,34 Sekunden der gesamten 5,22 Sekunden Laufzeit.
Snakeviz lässt uns sogar einen Blick in diese feste Funktion werfen. Der neue wichtigste Beitragszahler ist der orangefarbene Balken mit der Bezeichnung
Zusammenfassung
Dieser Artikel bietet eine praktische Anleitung zum Identifizieren und Beheben von Leistungsproblemen in Python-Code und argumentiert, dass Entwickler dazu Profilierungstools verwenden sollten messen Leistung zu verbessern, anstatt sich auf Instinct oder Vermutungen zu verlassen, um die Ursache von Verlangsamungen zu ermitteln.
Ich habe einen methodischen Arbeitsablauf mit zwei Schlüsselwerkzeugen demonstriert:-
- cProfile: Pythons integrierter Profiler, der zum Sammeln detaillierter Daten zu Funktionsaufrufen und Ausführungszeiten verwendet wird.
- Schlangenviz: Ein Visualisierungstool, das die Daten von cProfile in ein interaktives „Eiszapfen“-Diagramm umwandelt und es so einfach macht, visuell zu erkennen, welche Teile des Codes die meiste Zeit verbrauchen.
Der Artikel verwendet eine Fallstudie eines absichtlich langsamen Skripts, das mit drei deutlichen und erheblichen Engpässen entwickelt wurde:
- Eine iterationsgebundene Aufgabe: Eine Funktion, die millionenfach in einer Schleife aufgerufen wird und die Leistungseinbußen des Funktionsaufruf-Overheads von Python („Tod durch tausend Schnitte“) zeigt.
- Eine CPU-gebundene Aufgabe: Eine for-Schleife, die Millionen mathematischer Berechnungen durchführt und die Ineffizienz von reinem Python für schwere numerische Arbeiten verdeutlicht.
- Eine speichergebundene Aufgabe: Eine große Zeichenfolge, die durch wiederholte +=-Verkettung ineffizient erstellt wurde.
Durch die Analyse der Snakeviz-Ausgabe habe ich diese drei Probleme lokalisiert und gezielte Korrekturen vorgenommen.
- Der Iterationsengpass wurde behoben durch Eliminierung der unnötigen Schleife.
- Der CPU-Engpass wurde durch Vektorisierung mit NumPy behoben, das mathematische Operationen in schnellem, kompiliertem C-Code ausführt.
- Der Speicherengpass wurde durch das Anhängen von Zeichenfolgenteilen an eine Liste und die Verwendung eines einzigen, effizienten „“ behoben..verbinden() Anruf.
Diese Korrekturen führten zu einer erheblichen Beschleunigung und verkürzten die Laufzeit des Skripts erheblich 30 Sekunden bis knapp vorbei 6 Sekunden. Abschließend habe ich gezeigt, dass der Profiler auch nach der Lösung größerer Probleme erneut zur Identifizierung verwendet werden kann neukleinere Engpässe, was verdeutlicht, dass die Leistungsoptimierung ein iterativer Prozess ist, der durch Messungen gesteuert wird.
