Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Profiling und Performance-Analyse

Heinrich-Heine-Universität Düsseldorf

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 snakeviz

Beispiel: 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 20
Mean: 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.py
Mean: 0.73, StdDev: 32.13

Der folgende Befehl öffnet einen Webbrowser.

uv run snakeviz simulation.prof

So sieht es aus: Screenshot des SnakeViz Interface

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 --cli
Mean: 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 view

Nicht-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.py
py-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
Das Resultat 'flamegraph.svg' lässt sich im Browser öffnen und interaktiv durchsuchen

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.bin
Writing 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