Gutes Performance-Tuning folgt der wissenschaftlichen Methode (messen, Hypothese bilden, optimieren, verifizieren). Profiling liefert uns die Messdaten, um Premature Optimization (Donald Knuth) zu vermeiden.
Wir betrachten Tools für Makro- und Mikroanalysen: von simplen Zeitmessungen über deterministisches Profiling bis hin zu statistischem Sampling.
# Vorbereitung: Virtuelle Umgebung und Tools installieren
!uv venv --quiet
!uv sync --quiet
!uv pip install --quiet line-profiler memray py-spy scalene snakevizBeispiel: Monte-Carlo Random Walk¶
Wir simulieren Random Walks. Der Code enthält absichtlich ineffiziente Implementierungsdetails (z. B. random.choice statt random.getrandbits oder vektorisierten Operationen), um einen typischen CPU-Flaschenhals zu erzeugen.
%%writefile simulation.py
import random
import math
def random_walk(steps):
position = 0
for _ in range(steps):
# Absichtlich ineffizient: Listen-Allokation bei jedem Schleifendurchlauf
position += random.choice([-1, 1])
return position
def simulate_multiple_walks(runs, steps):
return [random_walk(steps) for _ in range(runs)]
def compute_statistics(data):
n = len(data)
mean = sum(data) / n
variance = sum((x - mean) ** 2 for x in data) / n
return mean, math.sqrt(variance)
def main():
data = simulate_multiple_walks(5000, 1000)
mean, std_dev = compute_statistics(data)
print(f"Mean: {mean:.2f}, StdDev: {std_dev:.2f}")
if __name__ == "__main__":
main()
Writing simulation.py
Einfache Messung mit timeit¶
Für einen schnellen Überblick reicht oft das eingebaute timeit-Modul. Es deaktiviert standardmäßig die Garbage Collection, um konsistentere Ergebnisse zu liefern. Man spricht von makroskopischer Messung weil die Gesamtlaufzeit eines Codeblocks gemessen wird (letztlich durch Erfassen der Systemzeit vor und nach dem Lauf). Es ist dabei üblich, mehrere Durchläufe eines Programms auszumessen und die Werte zu mitteln (meist mit einem harmonischen Mittel oder einfach dem Minimum - je nachdem, was man wissen möchte).
# Gesamtlaufzeit messen (1 Durchlauf)
!uv run python -m timeit -n 1 -r 1 "import simulation; simulation.main()"Mean: 0.16, StdDev: 31.25
1 loop, best of 1: 951 msec per loop
HIER SOLL NOCH HIN: jupyter cell magic mit time/timeit, danach Abschnitt über line_profiler; außerdem sollte das Verhalten von time/timeit, line_profiler, cProfile und ggf. anderen noch klarer voneinander abgegrenzt werden.
Deterministisches Profiling mit cProfile¶
cProfile ist in der C-Implementierung von Python integriert. Es misst jeden einzelnen Funktionsaufruf (deterministisch). Der Overhead ist spürbar, liefert aber exakte Call-Counts.
Wir sortieren die Ausgabe direkt nach der kumulierten Zeit (cumtime).
!uv run python -m cProfile -s cumtime simulation.py 2> /dev/null | head -n 20Mean: 0.26, StdDev: 31.86
35009204 function calls (35009181 primitive calls) in 5.236 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
3/1 0.000 0.000 5.236 5.236 {built-in method builtins.exec}
1 0.000 0.000 5.236 5.236 simulation.py:1(<module>)
1 0.000 0.000 5.236 5.236 simulation.py:20(main)
1 0.001 0.001 5.235 5.235 simulation.py:11(simulate_multiple_walks)
5000 0.688 0.000 5.234 0.001 simulation.py:4(random_walk)
5000000 1.543 0.000 4.546 0.000 random.py:345(choice)
5000000 1.616 0.000 2.491 0.000 random.py:245(_randbelow_with_getrandbits)
9998442 0.534 0.000 0.534 0.000 {method 'getrandbits' of '_random.Random' objects}
10000024 0.513 0.000 0.513 0.000 {built-in method builtins.len}
5000000 0.340 0.000 0.340 0.000 {method 'bit_length' of 'int' objects}
1 0.000 0.000 0.001 0.001 simulation.py:14(compute_statistics)
2 0.000 0.000 0.001 0.000 {built-in method builtins.sum}
5/1 0.000 0.000 0.001 0.001 <frozen importlib._bootstrap>:1349(_find_and_load)
5/1 0.000 0.000 0.001 0.001 <frozen importlib._bootstrap>:1304(_find_and_load_unlocked)
Visuelle Analyse mit SnakeViz¶
Text-Tabellen sind bei tiefen Call-Stacks unübersichtlich. Wir speichern die cProfile-Daten binär (-o) und nutzen snakeviz für eine interaktive Icicle-/Sunburst-Darstellung im Browser.
!uv run python -m cProfile -o simulation.prof simulation.pyMean: 0.73, StdDev: 32.13
Der folgende Befehl öffnet einen Webbrowser.
uv run snakeviz simulation.profSo sieht es aus:

Mikroanalyse auf Zeilenebene¶
Wenn wir wissen, dass random_walk langsam ist, wollen wir exakt sehen, welche Zeile Zeit kostet.
scalene ist ein moderner High-Performance Profiler, der CPU (Python vs. C-Code) und Speicherallokationen pro Zeile aufschlüsselt.
# Scalene generiert eine übersichtliche CLI-Ausgabe oder ein HTML-Profil.
!uv run scalene run --cli --reduced-profile simulation.py
!uv run scalene view --cliMean: 0.12, StdDev: 31.80
Scalene: profile saved to scalene-profile.json
To view in browser: scalene view
To view in terminal: scalene view --cli
NOTE: The GPU is currently running in a mode that can reduce Scalene's accuracy when reporting GPU utilization.
If you have sudo privileges, you can run this command (Linux only) to enable per-process GPU accounting:
python3 -m scalene.set_nvidia_gpu_modes
/home/konrad/Sciebo/hhu/eipy-26/eipy-skript/part-tools/simulation.py: % of ti
100.00% (1.458s) out of 1.458s.
╷ ╷ ╷ ╷ ╷ ╷ ╷ ╷ ╷
│Time │––––… │–––––– │––––… │Await │Memory │–––––– │––––––––––– │Co
Line │Python │nati… │system │GPU │% │Python │peak │timeline/% │(M
╺━━━━━━┿━━━━━━━┿━━━━━━┿━━━━━━━┿━━━━━━┿━━━━━━━┿━━━━━━━┿━━━━━━━┿━━━━━━━━━━━━━━━┿━━
1 │ │ │ │ │ │ │ │ │
2 │ │ │ │ │ │ │ │ │
3 │ │ │ │ │ │ │ │ │
4 │ │ │ │ │ │ │ │ │
5 │ │ │ │ │ │ │ │ │
6 │ │ │ │ │ │ │ │ │
7 │ │ │ │ │ │ │ │ │
8 │ 13% │ 8… │ 2% │ 6% │ │ │ │ │
9 │ │ │ │ │ │ │ │ │
10 │ │ │ │ │ │ │ │ │
11 │ │ │ │ │ │ │ │ │
12 │ │ │ │ │ │ │ │ │
13 │ │ │ │ │ │ │ │ │
14 │ │ │ │ │ │ │ │ │
15 │ │ │ │ │ │ │ │ │
16 │ │ │ │ │ │ │ │ │
17 │ │ │ │ │ │ │ │ │
18 │ │ │ │ │ │ │ │ │
19 │ │ │ │ │ │ │ │ │
20 │ │ │ │ │ │ │ │ │
21 │ │ │ │ │ │ │ │ │
22 │ │ │ │ │ │ │ │ │
23 │ │ │ │ │ │ │ │ │
24 │ │ │ │ │ │ │ │ │
25 │ │ │ │ │ │ │ │ │
26 │ │ │ │ │ │ │ │ │
╵ ╵ ╵ ╵ ╵ ╵ ╵ ╵ ╵
Function summaries:
random_walk (line 4): 13% Python, 84% native
Etwas ansprechender ist die interaktive grafische Oberfläche:
uv run scalene viewNicht-intrusives Sampling: Py-Spy¶
cProfile verändert durch den Overhead das Laufzeitverhalten. Sampling-Profiler wie py-spy pausieren den Python-Prozess stattdessen extrem kurz (z.B. 100 Mal pro Sekunde) und lesen den C-Stack aus.
Vorteile: Nahezu Zero-Overhead, kann an bereits laufende Prozesse (via PID) angehängt werden und generiert direkt Flame Graphs.
# Py-spy benötigt oft Root-Rechte, um Speicher fremder Prozesse zu lesen (sudo).
# Im User-Space (wie hier) generieren wir einen Flamegraph direkt beim Starten:
!uv run py-spy record -o flamegraph.svg -- python simulation.pypy-spy> Sampling process 100 times a second. Press Control-C to exit.
Mean: 0.42, StdDev: 31.55
py-spy> Stopped sampling because process exited
py-spy> Wrote flamegraph data to 'flamegraph.svg'. Samples: 98 Errors: 0
Memory Profiling mit Memray¶
Während Tools wie Scalene CPU und Speicher kombiniert analysieren, ist memray ein hochspezialisierter C-basierter Memory-Profiler.
Er fängt jede Speicherallokation ab – sowohl im reinen Python-Code als auch in C-Erweiterungen (wie NumPy). Das Tool modifiziert den Quellcode nicht und eignet sich hervorragend, um Memory Leaks oder temporäre Allokations-Spitzen exakt zu lokalisieren.
Im generierten Flamegraph lässt sich nicht nur die Gesamtmenge des allokierten Speichers ablesen, sondern auch visuell zwischen Python-Allokationen und nativen C-Allokationen unterscheiden.
!uvx memray run -o simulation.bin simulation.py
!uvx memray flamegraph simulation.binWriting profile results into simulation.bin
Memray WARNING: Correcting symbol for malloc from 0x4247d0 to 0x72ac068ad670
Memray WARNING: Correcting symbol for free from 0x424520 to 0x72ac068add50
Mean: 0.72, StdDev: 31.70
[memray] Successfully generated profile results.
You can now generate reports from the stored allocation records.
Some example commands to generate reports:
/home/konrad/.cache/uv/archive-v0/wXZ7NNq1jZwFohZOnNcWO/bin/python -m memray flamegraph simulation.bin
File already exists, will not overwrite: memray-flamegraph-simulation.html
iframe
Ausblick: Continuous Profiling und eBPF¶
Während py-spy hervorragend für die lokale Analyse oder punktuelles Debugging einzelner Prozesse geeignet ist, erfordern verteilte Cloud-Systeme eine kontinuierliche Überwachung.
Moderne Tools wie Parca oder Prodfiler nutzen eBPF (Extended Berkeley Packet Filter). Anstatt den Python-Prozess aus dem User-Space zu pollen, klinken sie sich direkt auf Kernel-Ebene in die Ausführung ein.
Vorteil: Nahezu Zero-Overhead (oft < 1%), da keine Context-Switches nötig sind.
Ergebnis: Aggregierte Flame Graphs in Echtzeit, die aufzeigen, wo ein gesamtes Cluster CPU-Zeit verbringt.
Mit tachyon steht außerdem bereits ein neuer Sampling-Profiler in den Startlöchern, der speziell auf die architektonischen Neuerungen ab Python 3.15 zugeschnitten sein wird.
# Aufräumen
!rm -f scalene-profile.html scalene-profile.json simulation.bin simulation.prof simulation.py