Python-Unterstützung für den Linux perf Profiler¶
- Autor:
Pablo Galindo
Der Linux perf Profiler ist ein sehr mächtiges Werkzeug, das es Ihnen ermöglicht, die Performance Ihrer Anwendung zu profilieren und Informationen darüber zu erhalten. perf verfügt auch über ein sehr lebendiges Ökosystem von Werkzeugen, die bei der Analyse der von ihm erzeugten Daten helfen.
Das Hauptproblem bei der Verwendung des perf Profilers mit Python-Anwendungen ist, dass perf nur Informationen über native Symbole erhält, d. h. die Namen von Funktionen und Prozeduren, die in C geschrieben sind. Das bedeutet, dass die Namen und Dateinamen von Python-Funktionen in Ihrem Code nicht in der Ausgabe von perf erscheinen werden.
Seit Python 3.12 kann der Interpreter in einem speziellen Modus ausgeführt werden, der es Python-Funktionen ermöglicht, in der Ausgabe des perf Profilers zu erscheinen. Wenn dieser Modus aktiviert ist, schaltet sich der Interpreter mit einem kleinen, dynamisch kompilierten Codefragment vor der Ausführung jeder Python-Funktion dazwischen und teilt perf die Beziehung zwischen diesem Codefragment und der zugehörigen Python-Funktion mithilfe von perf map Dateien mit.
Hinweis
Die Unterstützung für den perf Profiler ist derzeit nur für Linux auf ausgewählten Architekturen verfügbar. Überprüfen Sie die Ausgabe des configure Build-Schritts oder prüfen Sie die Ausgabe von python -m sysconfig | grep HAVE_PERF_TRAMPOLINE, um zu sehen, ob Ihr System unterstützt wird.
Betrachten Sie zum Beispiel das folgende Skript
def foo(n):
result = 0
for _ in range(n):
result += 1
return result
def bar(n):
foo(n)
def baz(n):
bar(n)
if __name__ == "__main__":
baz(1000000)
Wir können perf verwenden, um CPU-Stack-Traces mit 9999 Hertz zu sampeln
$ perf record -F 9999 -g -o perf.data python my_script.py
Dann können wir perf report verwenden, um die Daten zu analysieren
$ perf report --stdio -n -g
# Children Self Samples Command Shared Object Symbol
# ........ ........ ............ .......... .................. ..........................................
#
91.08% 0.00% 0 python.exe python.exe [.] _start
|
---_start
|
--90.71%--__libc_start_main
Py_BytesMain
|
|--56.88%--pymain_run_python.constprop.0
| |
| |--56.13%--_PyRun_AnyFileObject
| | _PyRun_SimpleFileObject
| | |
| | |--55.02%--run_mod
| | | |
| | | --54.65%--PyEval_EvalCode
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | |
| | | |--51.67%--_PyEval_EvalFrameDefault
| | | | |
| | | | |--11.52%--_PyLong_Add
| | | | | |
| | | | | |--2.97%--_PyObject_Malloc
...
Wie Sie sehen können, werden die Python-Funktionen nicht in der Ausgabe angezeigt, nur _PyEval_EvalFrameDefault (die Funktion, die den Python-Bytecode auswertet) erscheint. Das ist leider nicht sehr nützlich, da alle Python-Funktionen die gleiche C-Funktion zur Auswertung des Bytecodes verwenden, so dass wir nicht wissen können, welche Python-Funktion welcher Bytecode-auswertenden Funktion entspricht.
Stattdessen, wenn wir das gleiche Experiment mit aktivierter perf Unterstützung durchführen, erhalten wir
$ perf report --stdio -n -g
# Children Self Samples Command Shared Object Symbol
# ........ ........ ............ .......... .................. .....................................................................
#
90.58% 0.36% 1 python.exe python.exe [.] _start
|
---_start
|
--89.86%--__libc_start_main
Py_BytesMain
|
|--55.43%--pymain_run_python.constprop.0
| |
| |--54.71%--_PyRun_AnyFileObject
| | _PyRun_SimpleFileObject
| | |
| | |--53.62%--run_mod
| | | |
| | | --53.26%--PyEval_EvalCode
| | | py::<module>:/src/script.py
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | py::baz:/src/script.py
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | py::bar:/src/script.py
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | py::foo:/src/script.py
| | | |
| | | |--51.81%--_PyEval_EvalFrameDefault
| | | | |
| | | | |--13.77%--_PyLong_Add
| | | | | |
| | | | | |--3.26%--_PyObject_Malloc
Aktivieren der perf Profiling-Unterstützung¶
Die perf Profiling-Unterstützung kann entweder von Anfang an über die Umgebungsvariable PYTHONPERFSUPPORT oder die Option -X perf aktiviert werden, oder dynamisch über sys.activate_stack_trampoline() und sys.deactivate_stack_trampoline().
Die sys-Funktionen haben Vorrang vor der -X-Option, die -X-Option hat Vorrang vor der Umgebungsvariablen.
Beispiel mit der Umgebungsvariable
$ PYTHONPERFSUPPORT=1 perf record -F 9999 -g -o perf.data python my_script.py
$ perf report -g -i perf.data
Beispiel mit der -X-Option
$ perf record -F 9999 -g -o perf.data python -X perf my_script.py
$ perf report -g -i perf.data
Beispiel mit den sys APIs in der Datei example.py
import sys
sys.activate_stack_trampoline("perf")
do_profiled_stuff()
sys.deactivate_stack_trampoline()
non_profiled_stuff()
...dann
$ perf record -F 9999 -g -o perf.data python ./example.py
$ perf report -g -i perf.data
Wie Sie die besten Ergebnisse erzielen¶
Für beste Ergebnisse sollte Python mit CFLAGS="-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer" kompiliert werden, da dies es Profilern ermöglicht, nur den Frame-Pointer und keine DWARF-Debug-Informationen zu verwenden. Dies liegt daran, dass der Code, der zur Unterstützung von perf eingeschleust wird, dynamisch generiert wird und keine DWARF-Debug-Informationen verfügbar hat.
Sie können überprüfen, ob Ihr System mit diesem Flag kompiliert wurde, indem Sie Folgendes ausführen:
$ python -m sysconfig | grep 'no-omit-frame-pointer'
Wenn Sie keine Ausgabe sehen, bedeutet dies, dass Ihr Interpreter nicht mit Frame-Pointern kompiliert wurde und daher möglicherweise nicht in der Lage ist, Python-Funktionen in der Ausgabe von perf anzuzeigen.
Arbeiten ohne Frame-Pointer¶
Wenn Sie mit einem Python-Interpreter arbeiten, der ohne Frame-Pointer kompiliert wurde, können Sie den perf Profiler trotzdem verwenden, aber der Overhead wird etwas höher sein, da Python zur Laufzeit Entwindungsinformationen für jeden Python-Funktionsaufruf generieren muss. Zusätzlich wird perf mehr Zeit für die Verarbeitung der Daten benötigen, da es die DWARF-Debug-Informationen zum Entwinden des Stacks verwenden muss, was ein langsamer Prozess ist.
Um diesen Modus zu aktivieren, können Sie die Umgebungsvariable PYTHON_PERF_JIT_SUPPORT oder die Option -X perf_jit verwenden, welche den JIT-Modus für den perf Profiler aktivieren.
Hinweis
Aufgrund eines Fehlers im perf Tool funktionieren nur perf Versionen höher als v6.8 mit dem JIT-Modus. Der Fix wurde auch in die Version v6.7.2 des Tools zurückportiert.
Beachten Sie, dass bei der Überprüfung der Version des perf Tools (was durch Ausführen von perf version erfolgen kann) Sie berücksichtigen müssen, dass einige Distributionen benutzerdefinierte Versionsnummern hinzufügen, die ein - Zeichen enthalten. Das bedeutet, dass perf 6.7-3 nicht unbedingt perf 6.7.3 ist.
Bei Verwendung des perf JIT-Modus benötigen Sie einen zusätzlichen Schritt, bevor Sie perf report ausführen können. Sie müssen den Befehl perf inject aufrufen, um die JIT-Informationen in die perf.data-Datei zu injizieren.
$ perf record -F 9999 -g -k 1 --call-graph dwarf -o perf.data python -Xperf_jit my_script.py
$ perf inject -i perf.data --jit --output perf.jit.data
$ perf report -g -i perf.jit.data
oder mit der Umgebungsvariable
$ PYTHON_PERF_JIT_SUPPORT=1 perf record -F 9999 -g --call-graph dwarf -o perf.data python my_script.py
$ perf inject -i perf.data --jit --output perf.jit.data
$ perf report -g -i perf.jit.data
Der Befehl perf inject --jit liest perf.data, holt sich automatisch die von Python erstellte Perf-Dump-Datei (in /tmp/perf-$PID.dump) und erstellt dann perf.jit.data, das alle JIT-Informationen zusammenführt. Es sollten auch viele jitted-XXXX-N.so-Dateien im aktuellen Verzeichnis erstellt werden, die ELF-Images für alle von Python erstellten JIT-Trampoline sind.
Warnung
Bei Verwendung von --call-graph dwarf nimmt das perf Tool Snapshots des Stacks des profilierten Prozesses und speichert die Informationen in der perf.data-Datei. Standardmäßig beträgt die Größe des Stack-Dumps 8192 Bytes, aber Sie können die Größe ändern, indem Sie sie nach einem Komma angeben, z. B. --call-graph dwarf,16384.
Die Größe des Stack-Dumps ist wichtig, denn wenn die Größe zu klein ist, kann perf den Stack nicht entwinden und die Ausgabe ist unvollständig. Wenn die Größe zu groß ist, kann perf den Prozess nicht so häufig sampeln, wie es möchte, da der Overhead höher ist.
Die Stack-Größe ist besonders wichtig, wenn Python-Code mit niedrigen Optimierungsstufen (wie -O0) kompiliert wird, da diese Builds tendenziell größere Stack-Frames haben. Wenn Sie Python mit -O0 kompilieren und keine Python-Funktionen in Ihrer Profiling-Ausgabe sehen, versuchen Sie, die Stack-Dump-Größe auf 65528 Bytes (das Maximum) zu erhöhen.
$ perf record -F 9999 -g -k 1 --call-graph dwarf,65528 -o perf.data python -Xperf_jit my_script.py
Unterschiedliche Kompilierungsflags können die Stack-Größen erheblich beeinflussen
Builds mit
-O0haben typischerweise deutlich größere Stack-Frames als Builds mit-O1oder höherHinzufügen von Optimierungen (
-O1,-O2usw.) reduziert typischerweise die Stack-GrößeFrame-Pointer (
-fno-omit-frame-pointer) sorgen im Allgemeinen für zuverlässigeres Stack-Entwinden