Python-stöd för Linux-profileraren perf
¶
- författare:
Pablo Galindo
Linux perf profiler är ett mycket kraftfullt verktyg som gör att du kan profilera och få information om prestandan i din applikation. perf
har också ett mycket levande ekosystem av verktyg som hjälper till med analysen av de data som den producerar.
Det största problemet med att använda profileraren perf
med Python-program är att perf
bara får information om inbyggda symboler, det vill säga namnen på funktioner och procedurer skrivna i C. Det innebär att namnen och filnamnen på Python-funktioner i din kod inte kommer att visas i utdata från perf
.
Sedan Python 3.12 kan tolken köras i ett speciellt läge som gör att Python-funktioner kan visas i utdata från profileraren perf
. När detta läge är aktiverat kommer tolken att lägga in en liten kodbit som kompileras i farten före exekveringen av varje Python-funktion och den kommer att lära perf
förhållandet mellan denna kodbit och den associerade Python-funktionen med hjälp av perf map files.
Anteckning
Stöd för profileraren perf
finns för närvarande endast för Linux på utvalda arkitekturer. Kontrollera utdata från byggsteget configure
eller kontrollera utdata från python -m sysconfig | grep HAVE_PERF_TRAMPOLINE
för att se om ditt system stöds.
Tänk till exempel på följande skript:
def foo(n):
resultat = 0
för _ i intervall(n):
resultat += 1
returnera resultat
def bar(n):
foo(n)
def baz(n):
bar(n)
om __name__ == "__main__":
baz(1000000)
Vi kan köra perf
för att prova CPU-stackspårningar vid 9999 hertz:
$ perf record -F 9999 -g -o perf.data python my_script.py
Sedan kan vi använda perf report
för att analysera data:
$ 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
...
Som du kan se visas inte Python-funktionerna i utdata, bara _PyEval_EvalFrameDefault
(funktionen som utvärderar Python-bytekoden) visas. Tyvärr är det inte särskilt användbart eftersom alla Python-funktioner använder samma C-funktion för att utvärdera bytecode, så vi kan inte veta vilken Python-funktion som motsvarar vilken bytecode-utvärderingsfunktion.
Istället, om vi kör samma experiment med perf
support aktiverat får vi:
$ 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
Så här aktiverar du profileringsstödet perf
¶
profileringsstödet perf
kan aktiveras antingen från början med hjälp av miljövariabeln PYTHONPERFSUPPORT
eller alternativet -X perf
, eller dynamiskt med hjälp av sys.activate_stack_trampoline()
och sys.deactivate_stack_trampoline()
.
Funktionerna sys
har företräde framför alternativet -X
, alternativet -X
har företräde framför miljövariabeln.
Exempel, med hjälp av miljövariabeln:
$ PYTHONPERFSUPPORT=1 perf record -F 9999 -g -o perf.data python my_script.py
$ perf rapport -g -i perf.data
Exempel på användning av -X
option:
$ perf record -F 9999 -g -o perf.data python -X perf my_script.py
$ perf rapport -g -i perf.data
Exempel på användning av API:erna sys
i filen example.py
:
import sys
sys.activate_stack_trampoline("perf")
do_profiled_stuff()
sys.deactivate_stack_trampoline()
non_profiled_stuff()
…sedan..:
$ perf record -F 9999 -g -o perf.data python ./example.py
$ perf rapport -g -i perf.data
Hur man uppnår bästa resultat¶
För bästa resultat bör Python kompileras med CFLAGS="-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer"
eftersom detta gör det möjligt för profilerare att rulla ut med endast rampekaren och inte på DWARF-felsökningsinformation. Detta beror på att koden som läggs in för att tillåta perf
-stöd genereras dynamiskt och därför inte har någon DWARF-felsökningsinformation tillgänglig.
Du kan kontrollera om ditt system har kompilerats med denna flagga genom att köra:
$ python -m sysconfig | grep 'no-omit-frame-pointer'
Om du inte ser någon utmatning betyder det att din tolk inte har kompilerats med rampekare och därför kanske den inte kan visa Python-funktioner i utmatningen från perf
.
Hur man arbetar utan rampekare¶
Om du arbetar med en Python-tolk som har kompilerats utan rampekare kan du fortfarande använda profileraren perf
, men omkostnaderna blir lite högre eftersom Python måste generera avrullningsinformation för varje Python-funktionsanrop i farten. Dessutom kommer det att ta längre tid för perf
att bearbeta data eftersom den måste använda DWARF-felsökningsinformationen för att spola tillbaka stacken och det är en långsam process.
För att aktivera det här läget kan du använda miljövariabeln PYTHON_PERF_JIT_SUPPORT
eller alternativet -X perf_jit
, som aktiverar JIT-läget för profileraren perf
.
Anteckning
På grund av en bugg i verktyget perf
är det bara perf
-versioner som är högre än v6.8 som fungerar med JIT-läget. Fixen har även backporterats till v6.7.2-versionen av verktyget.
Observera att när du kontrollerar versionen av verktyget perf
(vilket kan göras genom att köra perf version
) måste du ta hänsyn till att vissa distros lägger till egna versionsnummer som innehåller tecknet -
. Detta innebär att perf 6.7-3
inte nödvändigtvis är perf 6.7.3
.
När du använder perf JIT-läget behöver du ett extra steg innan du kan köra perf report
. Du måste anropa kommandot perf inject
för att injicera JIT-informationen i filen perf.data
:
$ 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 rapport -g -i perf.jit.data
eller med hjälp av miljövariabeln:
$ 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 rapport -g -i perf.jit.data
kommandot perf inject --jit
kommer att läsa perf.data
, automatiskt plocka upp den perf dump-fil som Python skapar (i /tmp/perf-$PID.dump
) och sedan skapa perf.jit.data
som sammanfogar all JIT-information. Det bör också skapa en hel del jitted-XXXX-N.so
-filer i den aktuella katalogen som är ELF-bilder för alla JIT-trampoliner som skapades av Python.
Varning
När du använder --call-graph dwarf
kommer verktyget perf
att ta ögonblicksbilder av stacken i den process som profileras och spara informationen i filen perf.data
. Som standard är storleken på stackdumpen 8192 byte, men du kan ändra storleken genom att ange den efter ett kommatecken som --call-graph dwarf,16384
.
Storleken på stackdumpen är viktig, för om den är för liten kan perf
inte spola tillbaka stacken och utdata blir ofullständiga. Å andra sidan, om storleken är för stor, kommer perf
inte att kunna sampla processen så ofta som den skulle vilja eftersom overhead blir högre.
Stackstorleken är särskilt viktig vid profilering av Python-kod som kompilerats med låga optimeringsnivåer (som -O0
), eftersom dessa kompileringar tenderar att ha större stackramar. Om du kompilerar Python med -O0
och inte ser Python-funktioner i din profileringsutdata, försök att öka stackdumpstorleken till 65528 byte (max):
$ perf record -F 9999 -g -k 1 --call-graph dwarf,65528 -o perf.data python -Xperf_jit my_script.py
Olika kompileringsflaggor kan påverka stackstorleken avsevärt:
Byggnader med
-O0
har vanligtvis mycket större stapelramar än de med-O1
eller högreAtt lägga till optimeringar (
-O1
,-O2
, etc.) minskar vanligtvis stackstorlekenFrame-pekare (
-fno-omit-frame-pointer
) ger i allmänhet mer tillförlitlig stackavveckling